create-entity-server 0.0.9 → 0.0.15
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.
- package/bin/create.js +11 -1
- package/package.json +1 -1
- package/template/.env.example +17 -1
- package/template/configs/jwt.json +1 -0
- package/template/configs/oauth.json +37 -0
- package/template/configs/push.json +26 -0
- package/template/entities/README.md +4 -4
- package/template/entities/{Auth → System/Auth}/account.json +0 -14
- package/template/samples/README.md +16 -0
- package/template/samples/entities/01_basic_fields.json +39 -0
- package/template/samples/entities/02_types_and_defaults.json +68 -0
- package/template/samples/entities/03_hash_and_unique.json +33 -0
- package/template/samples/entities/04_fk_and_composite_unique.json +31 -0
- package/template/samples/entities/05_cache.json +54 -0
- package/template/samples/entities/06_history_and_hard_delete.json +42 -0
- package/template/samples/entities/07_license_scope.json +43 -0
- package/template/samples/entities/08_hook_sql.json +52 -0
- package/template/samples/entities/09_hook_entity.json +71 -0
- package/template/samples/entities/10_hook_submit_delete.json +75 -0
- package/template/samples/entities/11_hook_webhook.json +82 -0
- package/template/samples/entities/12_hook_push.json +73 -0
- package/template/samples/entities/13_read_only.json +51 -0
- package/template/samples/entities/14_optimistic_lock.json +29 -0
- package/template/samples/entities/15_reset_defaults.json +95 -0
- package/template/samples/entities/README.md +94 -0
- package/template/samples/entities/order_notification.json +51 -0
- package/template/samples/flutter/lib/entity_server_client.dart +91 -0
- package/template/samples/java/EntityServerClient.java +117 -0
- package/template/samples/kotlin/EntityServerClient.kt +86 -0
- package/template/samples/node/src/EntityServerClient.js +116 -0
- package/template/samples/php/ci4/Config/EntityServer.php +15 -0
- package/template/samples/php/ci4/Controllers/EntityController.php +202 -0
- package/template/samples/php/ci4/Controllers/ProductController.php +16 -76
- package/template/samples/php/ci4/Libraries/EntityServer.php +150 -11
- package/template/samples/php/laravel/Services/EntityServerService.php +56 -0
- package/template/samples/python/entity_server.py +106 -0
- package/template/samples/react/src/api/entityServerClient.ts +123 -0
- package/template/samples/react/src/hooks/useEntity.ts +68 -0
- package/template/samples/swift/EntityServerClient.swift +105 -0
- package/template/scripts/normalize-entities.sh +10 -10
- package/template/scripts/run.sh +108 -29
- package/template/scripts/update-server.ps1 +92 -2
- package/template/scripts/update-server.sh +73 -2
- /package/template/entities/{Auth → System/Auth}/api_keys.json +0 -0
- /package/template/entities/{Auth → System/Auth}/license.json +0 -0
- /package/template/entities/{Auth → System/Auth}/rbac_roles.json +0 -0
|
@@ -195,6 +195,110 @@ public class EntityServerClient {
|
|
|
195
195
|
return request("POST", "/v1/entity/" + entity + "/rollback/" + historySeq, null);
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
/** 푸시 발송 트리거 엔티티에 submit합니다. */
|
|
199
|
+
public String push(String pushEntity, String payloadJson) throws IOException {
|
|
200
|
+
return push(pushEntity, payloadJson, null);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** 푸시 발송 트리거 엔티티에 submit합니다. (트랜잭션 지원) */
|
|
204
|
+
public String push(String pushEntity, String payloadJson, String transactionId) throws IOException {
|
|
205
|
+
return submit(pushEntity, payloadJson, transactionId);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** push_log 목록 조회 헬퍼 */
|
|
209
|
+
public String pushLogList() throws IOException {
|
|
210
|
+
return pushLogList(1, 20, null);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** push_log 목록 조회 헬퍼 */
|
|
214
|
+
public String pushLogList(int page, int limit, String orderBy) throws IOException {
|
|
215
|
+
return list("push_log", page, limit, orderBy);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** account_device 디바이스 등록/갱신 헬퍼 (push_token 단일 필드) */
|
|
219
|
+
public String registerPushDevice(
|
|
220
|
+
long accountSeq,
|
|
221
|
+
String deviceId,
|
|
222
|
+
String pushToken,
|
|
223
|
+
String platform,
|
|
224
|
+
String deviceType,
|
|
225
|
+
boolean pushEnabled,
|
|
226
|
+
String transactionId
|
|
227
|
+
) throws IOException {
|
|
228
|
+
StringBuilder payload = new StringBuilder("{");
|
|
229
|
+
payload.append("\"id\":").append(jsonString(deviceId));
|
|
230
|
+
payload.append(",\"account_seq\":").append(accountSeq);
|
|
231
|
+
payload.append(",\"push_token\":").append(jsonString(pushToken));
|
|
232
|
+
payload.append(",\"push_enabled\":").append(pushEnabled);
|
|
233
|
+
if (platform != null && !platform.isBlank()) {
|
|
234
|
+
payload.append(",\"platform\":").append(jsonString(platform));
|
|
235
|
+
}
|
|
236
|
+
if (deviceType != null && !deviceType.isBlank()) {
|
|
237
|
+
payload.append(",\"device_type\":").append(jsonString(deviceType));
|
|
238
|
+
}
|
|
239
|
+
payload.append("}");
|
|
240
|
+
|
|
241
|
+
return submit("account_device", payload.toString(), transactionId);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** account_device.seq 기준 push_token 갱신 헬퍼 */
|
|
245
|
+
public String updatePushDeviceToken(
|
|
246
|
+
long deviceSeq,
|
|
247
|
+
String pushToken,
|
|
248
|
+
boolean pushEnabled,
|
|
249
|
+
String transactionId
|
|
250
|
+
) throws IOException {
|
|
251
|
+
String payload = "{" +
|
|
252
|
+
"\"seq\":" + deviceSeq +
|
|
253
|
+
",\"push_token\":" + jsonString(pushToken) +
|
|
254
|
+
",\"push_enabled\":" + pushEnabled +
|
|
255
|
+
"}";
|
|
256
|
+
return submit("account_device", payload, transactionId);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** account_device.seq 기준 푸시 수신 비활성화 헬퍼 */
|
|
260
|
+
public String disablePushDevice(long deviceSeq, String transactionId) throws IOException {
|
|
261
|
+
String payload = "{" +
|
|
262
|
+
"\"seq\":" + deviceSeq +
|
|
263
|
+
",\"push_enabled\":false" +
|
|
264
|
+
"}";
|
|
265
|
+
return submit("account_device", payload, transactionId);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 요청 본문을 읽어 JSON 문자열로 반환합니다.
|
|
270
|
+
* - application/octet-stream: 암호 패킷 복호화
|
|
271
|
+
* - 그 외: 평문 JSON 문자열 반환
|
|
272
|
+
*/
|
|
273
|
+
public String readRequestBody(byte[] rawBody, String contentType, boolean requireEncrypted) throws IOException {
|
|
274
|
+
String lowered = contentType == null ? "" : contentType.toLowerCase();
|
|
275
|
+
boolean isEncrypted = lowered.contains("application/octet-stream");
|
|
276
|
+
|
|
277
|
+
if (requireEncrypted && !isEncrypted) {
|
|
278
|
+
throw new IOException("Encrypted request required: Content-Type must be application/octet-stream");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (isEncrypted) {
|
|
282
|
+
if (rawBody == null || rawBody.length == 0) {
|
|
283
|
+
throw new IOException("Encrypted request body is empty");
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
return decryptPacket(rawBody);
|
|
287
|
+
} catch (Exception e) {
|
|
288
|
+
throw new IOException("Packet decryption failed: " + e.getMessage(), e);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (rawBody == null || rawBody.length == 0) {
|
|
293
|
+
return "{}";
|
|
294
|
+
}
|
|
295
|
+
return new String(rawBody, StandardCharsets.UTF_8);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
public String readRequestBody(byte[] rawBody, String contentType) throws IOException {
|
|
299
|
+
return readRequestBody(rawBody, contentType, false);
|
|
300
|
+
}
|
|
301
|
+
|
|
198
302
|
// ─── 내부 ─────────────────────────────────────────────────────────────────
|
|
199
303
|
|
|
200
304
|
private String request(String method, String path, String body) throws IOException {
|
|
@@ -301,4 +405,17 @@ public class EntityServerClient {
|
|
|
301
405
|
String v = System.getenv(key);
|
|
302
406
|
return (v != null && !v.isBlank()) ? v : defaultValue;
|
|
303
407
|
}
|
|
408
|
+
|
|
409
|
+
private static String jsonString(String value) {
|
|
410
|
+
if (value == null) {
|
|
411
|
+
return "null";
|
|
412
|
+
}
|
|
413
|
+
String escaped = value
|
|
414
|
+
.replace("\\", "\\\\")
|
|
415
|
+
.replace("\"", "\\\"")
|
|
416
|
+
.replace("\n", "\\n")
|
|
417
|
+
.replace("\r", "\\r")
|
|
418
|
+
.replace("\t", "\\t");
|
|
419
|
+
return "\"" + escaped + "\"";
|
|
420
|
+
}
|
|
304
421
|
}
|
|
@@ -118,6 +118,92 @@ class EntityServerClient(
|
|
|
118
118
|
fun rollback(entity: String, historySeq: Long): JSONObject =
|
|
119
119
|
request("POST", "/v1/entity/$entity/rollback/$historySeq")
|
|
120
120
|
|
|
121
|
+
/** 푸시 발송 트리거 엔티티에 submit합니다. */
|
|
122
|
+
fun push(pushEntity: String, payload: JSONObject, transactionId: String? = null): JSONObject =
|
|
123
|
+
submit(pushEntity, payload, transactionId)
|
|
124
|
+
|
|
125
|
+
/** push_log 목록 조회 헬퍼 */
|
|
126
|
+
fun pushLogList(page: Int = 1, limit: Int = 20): JSONObject =
|
|
127
|
+
list("push_log", page, limit)
|
|
128
|
+
|
|
129
|
+
/** account_device 디바이스 등록/갱신 헬퍼 (push_token 단일 필드) */
|
|
130
|
+
fun registerPushDevice(
|
|
131
|
+
accountSeq: Long,
|
|
132
|
+
deviceId: String,
|
|
133
|
+
pushToken: String,
|
|
134
|
+
platform: String? = null,
|
|
135
|
+
deviceType: String? = null,
|
|
136
|
+
pushEnabled: Boolean = true,
|
|
137
|
+
transactionId: String? = null,
|
|
138
|
+
): JSONObject {
|
|
139
|
+
val payload = JSONObject().apply {
|
|
140
|
+
put("id", deviceId)
|
|
141
|
+
put("account_seq", accountSeq)
|
|
142
|
+
put("push_token", pushToken)
|
|
143
|
+
put("push_enabled", pushEnabled)
|
|
144
|
+
if (!platform.isNullOrBlank()) put("platform", platform)
|
|
145
|
+
if (!deviceType.isNullOrBlank()) put("device_type", deviceType)
|
|
146
|
+
}
|
|
147
|
+
return submit("account_device", payload, transactionId)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** account_device.seq 기준 push_token 갱신 헬퍼 */
|
|
151
|
+
fun updatePushDeviceToken(
|
|
152
|
+
deviceSeq: Long,
|
|
153
|
+
pushToken: String,
|
|
154
|
+
pushEnabled: Boolean = true,
|
|
155
|
+
transactionId: String? = null,
|
|
156
|
+
): JSONObject =
|
|
157
|
+
submit(
|
|
158
|
+
"account_device",
|
|
159
|
+
JSONObject().apply {
|
|
160
|
+
put("seq", deviceSeq)
|
|
161
|
+
put("push_token", pushToken)
|
|
162
|
+
put("push_enabled", pushEnabled)
|
|
163
|
+
},
|
|
164
|
+
transactionId,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
/** account_device.seq 기준 푸시 수신 비활성화 헬퍼 */
|
|
168
|
+
fun disablePushDevice(
|
|
169
|
+
deviceSeq: Long,
|
|
170
|
+
transactionId: String? = null,
|
|
171
|
+
): JSONObject =
|
|
172
|
+
submit(
|
|
173
|
+
"account_device",
|
|
174
|
+
JSONObject().apply {
|
|
175
|
+
put("seq", deviceSeq)
|
|
176
|
+
put("push_enabled", false)
|
|
177
|
+
},
|
|
178
|
+
transactionId,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 요청 본문을 읽어 JSON으로 반환합니다.
|
|
183
|
+
* - application/octet-stream: 암호 패킷 복호화
|
|
184
|
+
* - 그 외: 평문 JSON 파싱
|
|
185
|
+
*/
|
|
186
|
+
fun readRequestBody(
|
|
187
|
+
rawBody: ByteArray,
|
|
188
|
+
contentType: String = "application/json",
|
|
189
|
+
requireEncrypted: Boolean = false,
|
|
190
|
+
): JSONObject {
|
|
191
|
+
val lowered = contentType.lowercase()
|
|
192
|
+
val isEncrypted = lowered.contains("application/octet-stream")
|
|
193
|
+
|
|
194
|
+
if (requireEncrypted && !isEncrypted) {
|
|
195
|
+
error("Encrypted request required: Content-Type must be application/octet-stream")
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (isEncrypted) {
|
|
199
|
+
if (rawBody.isEmpty()) error("Encrypted request body is empty")
|
|
200
|
+
return JSONObject(decryptPacket(rawBody))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (rawBody.isEmpty()) return JSONObject()
|
|
204
|
+
return JSONObject(String(rawBody, Charsets.UTF_8))
|
|
205
|
+
}
|
|
206
|
+
|
|
121
207
|
// ─── 내부 ─────────────────────────────────────────────────────────
|
|
122
208
|
|
|
123
209
|
private fun request(method: String, path: String, bodyStr: String = "", extraHeaders: Map<String, String> = emptyMap()): JSONObject {
|
|
@@ -178,6 +178,122 @@ export class EntityServerClient {
|
|
|
178
178
|
);
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/** 푸시 발송 트리거 엔티티에 submit합니다. */
|
|
182
|
+
push(pushEntity, payload, { transactionId } = {}) {
|
|
183
|
+
return this.submit(pushEntity, payload, { transactionId });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** push_log 목록 조회 헬퍼 */
|
|
187
|
+
pushLogList({ page = 1, limit = 20, orderBy } = {}) {
|
|
188
|
+
return this.list("push_log", { page, limit, orderBy });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** account_device 디바이스 등록/갱신 헬퍼 (push_token 단일 필드) */
|
|
192
|
+
registerPushDevice(
|
|
193
|
+
accountSeq,
|
|
194
|
+
deviceId,
|
|
195
|
+
pushToken,
|
|
196
|
+
{
|
|
197
|
+
platform,
|
|
198
|
+
deviceType,
|
|
199
|
+
browser,
|
|
200
|
+
browserVersion,
|
|
201
|
+
pushEnabled = true,
|
|
202
|
+
transactionId,
|
|
203
|
+
} = {},
|
|
204
|
+
) {
|
|
205
|
+
return this.submit(
|
|
206
|
+
"account_device",
|
|
207
|
+
{
|
|
208
|
+
id: deviceId,
|
|
209
|
+
account_seq: accountSeq,
|
|
210
|
+
push_token: pushToken,
|
|
211
|
+
push_enabled: pushEnabled,
|
|
212
|
+
...(platform ? { platform } : {}),
|
|
213
|
+
...(deviceType ? { device_type: deviceType } : {}),
|
|
214
|
+
...(browser ? { browser } : {}),
|
|
215
|
+
...(browserVersion ? { browser_version: browserVersion } : {}),
|
|
216
|
+
},
|
|
217
|
+
{ transactionId },
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** account_device.seq 기준 push_token 갱신 헬퍼 */
|
|
222
|
+
updatePushDeviceToken(
|
|
223
|
+
deviceSeq,
|
|
224
|
+
pushToken,
|
|
225
|
+
{ pushEnabled = true, transactionId } = {},
|
|
226
|
+
) {
|
|
227
|
+
return this.submit(
|
|
228
|
+
"account_device",
|
|
229
|
+
{
|
|
230
|
+
seq: deviceSeq,
|
|
231
|
+
push_token: pushToken,
|
|
232
|
+
push_enabled: pushEnabled,
|
|
233
|
+
},
|
|
234
|
+
{ transactionId },
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** account_device.seq 기준 푸시 수신 비활성화 헬퍼 */
|
|
239
|
+
disablePushDevice(deviceSeq, { transactionId } = {}) {
|
|
240
|
+
return this.submit(
|
|
241
|
+
"account_device",
|
|
242
|
+
{
|
|
243
|
+
seq: deviceSeq,
|
|
244
|
+
push_enabled: false,
|
|
245
|
+
},
|
|
246
|
+
{ transactionId },
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* 요청 본문을 읽어 JSON으로 반환합니다.
|
|
252
|
+
* - application/octet-stream: 암호 패킷 복호화
|
|
253
|
+
* - 그 외: 평문 JSON 파싱
|
|
254
|
+
*/
|
|
255
|
+
readRequestBody(
|
|
256
|
+
body,
|
|
257
|
+
contentType = "application/json",
|
|
258
|
+
{ requireEncrypted = false } = {},
|
|
259
|
+
) {
|
|
260
|
+
const lowered = String(contentType || "").toLowerCase();
|
|
261
|
+
const isEncrypted = lowered.includes("application/octet-stream");
|
|
262
|
+
|
|
263
|
+
if (requireEncrypted && !isEncrypted) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
"Encrypted request required: Content-Type must be application/octet-stream",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (isEncrypted) {
|
|
270
|
+
if (body == null) {
|
|
271
|
+
throw new Error("Encrypted request body is empty");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (body instanceof ArrayBuffer) {
|
|
275
|
+
return this.#decryptPacket(body);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (ArrayBuffer.isView(body)) {
|
|
279
|
+
const view = body;
|
|
280
|
+
const sliced = view.buffer.slice(
|
|
281
|
+
view.byteOffset,
|
|
282
|
+
view.byteOffset + view.byteLength,
|
|
283
|
+
);
|
|
284
|
+
return this.#decryptPacket(sliced);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
throw new Error(
|
|
288
|
+
"Encrypted request body must be ArrayBuffer, Buffer, or Uint8Array",
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (body == null || body === "") return {};
|
|
293
|
+
if (typeof body === "object") return body;
|
|
294
|
+
return JSON.parse(String(body));
|
|
295
|
+
}
|
|
296
|
+
|
|
181
297
|
// ─── 내부 ─────────────────────────────────────────────────────────────────
|
|
182
298
|
|
|
183
299
|
async #request(method, path, body, extraHeaders = {}) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace Config;
|
|
4
|
+
|
|
5
|
+
use CodeIgniter\Config\BaseConfig;
|
|
6
|
+
|
|
7
|
+
class EntityServer extends BaseConfig
|
|
8
|
+
{
|
|
9
|
+
public string $baseUrl = 'http://localhost:47200';
|
|
10
|
+
public string $apiKey = '';
|
|
11
|
+
public string $hmacSecret = '';
|
|
12
|
+
public int $timeout = 10;
|
|
13
|
+
public int $magicLen = 4;
|
|
14
|
+
public bool $requireEncryptedRequest = true;
|
|
15
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace App\Controllers;
|
|
4
|
+
|
|
5
|
+
use App\Controllers\BaseController;
|
|
6
|
+
use App\Libraries\EntityServer;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* EntityServer CRUD를 CI4 컨트롤러에서 공통으로 사용하는 베이스 컨트롤러.
|
|
10
|
+
*
|
|
11
|
+
* 사용법:
|
|
12
|
+
* class ProductController extends EntityController {
|
|
13
|
+
* protected string $entity = 'product';
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
abstract class EntityController extends BaseController
|
|
17
|
+
{
|
|
18
|
+
protected EntityServer $es;
|
|
19
|
+
|
|
20
|
+
/** 대상 엔티티명 (하위 컨트롤러에서 지정) */
|
|
21
|
+
protected string $entity = '';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 푸시 트리거용 엔티티명 (기본: 시스템 엔티티 push_msg)
|
|
25
|
+
*
|
|
26
|
+
* 주의: push 인프라 필수 엔티티는 account_device / push_msg / push_log 이며,
|
|
27
|
+
* 이 값은 "어떤 엔티티 insert로 push hook를 트리거할지"를 결정합니다.
|
|
28
|
+
*/
|
|
29
|
+
protected string $pushEntity = 'push_msg';
|
|
30
|
+
|
|
31
|
+
/** pushEntity에서 수신자를 가리키는 필드명 */
|
|
32
|
+
protected string $pushTargetField = 'account_seq';
|
|
33
|
+
|
|
34
|
+
public function __construct()
|
|
35
|
+
{
|
|
36
|
+
$this->es = new EntityServer();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** GET /{entity}/list?page=1&limit=20 */
|
|
40
|
+
public function list(): string
|
|
41
|
+
{
|
|
42
|
+
$page = (int) ($this->request->getGet('page') ?? 1);
|
|
43
|
+
$limit = (int) ($this->request->getGet('limit') ?? 20);
|
|
44
|
+
|
|
45
|
+
$result = $this->es->list($this->entity, [
|
|
46
|
+
'page' => $page,
|
|
47
|
+
'limit' => $limit,
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
return $this->response->setJSON($result)->getBody();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** GET /{entity}/get/(:num) */
|
|
54
|
+
public function get(int $seq): string
|
|
55
|
+
{
|
|
56
|
+
$result = $this->es->get($this->entity, $seq);
|
|
57
|
+
return $this->response->setJSON($result)->getBody();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** POST /{entity}/query */
|
|
61
|
+
public function query(): string
|
|
62
|
+
{
|
|
63
|
+
try {
|
|
64
|
+
$body = $this->es->readRequestBody($this->request);
|
|
65
|
+
} catch (\Throwable $e) {
|
|
66
|
+
return $this->response->setStatusCode(400)
|
|
67
|
+
->setJSON(['ok' => false, 'message' => $e->getMessage()])
|
|
68
|
+
->getBody();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
$filter = $body['filter'] ?? [];
|
|
72
|
+
$params = [
|
|
73
|
+
'page' => $body['page'] ?? 1,
|
|
74
|
+
'limit' => $body['limit'] ?? 20,
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
$result = $this->es->query($this->entity, $filter, $params);
|
|
78
|
+
return $this->response->setJSON($result)->getBody();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* POST /{entity}/submit
|
|
83
|
+
* POST /{entity}/submit/(:num) (seq route param을 body에 주입)
|
|
84
|
+
*/
|
|
85
|
+
public function submit(?int $seq = null): string
|
|
86
|
+
{
|
|
87
|
+
try {
|
|
88
|
+
$data = $this->es->readRequestBody($this->request);
|
|
89
|
+
} catch (\Throwable $e) {
|
|
90
|
+
return $this->response->setStatusCode(400)
|
|
91
|
+
->setJSON(['ok' => false, 'message' => $e->getMessage()])
|
|
92
|
+
->getBody();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if ($seq !== null) {
|
|
96
|
+
$data['seq'] = $seq;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
$result = $this->es->submit($this->entity, $data);
|
|
100
|
+
return $this->response->setJSON($result)->getBody();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** DELETE /{entity}/delete/(:num) */
|
|
104
|
+
public function delete(int $seq): string
|
|
105
|
+
{
|
|
106
|
+
$result = $this->es->delete($this->entity, $seq);
|
|
107
|
+
return $this->response->setJSON($result)->getBody();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** GET /{entity}/history/(:num) */
|
|
111
|
+
public function history(int $seq): string
|
|
112
|
+
{
|
|
113
|
+
$result = $this->es->history($this->entity, $seq);
|
|
114
|
+
return $this->response->setJSON($result)->getBody();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** POST /{entity}/rollback/(:num) */
|
|
118
|
+
public function rollback(int $historySeq): string
|
|
119
|
+
{
|
|
120
|
+
$result = $this->es->rollback($this->entity, $historySeq);
|
|
121
|
+
return $this->response->setJSON($result)->getBody();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* POST /{entity}/push
|
|
126
|
+
*
|
|
127
|
+
* push hook가 연결된 엔티티(push_msg 등)에 submit하여 푸시를 발행합니다.
|
|
128
|
+
* 요청 예:
|
|
129
|
+
* {
|
|
130
|
+
* "account_seq": 1,
|
|
131
|
+
* "title": "알림 제목",
|
|
132
|
+
* "message": "알림 본문",
|
|
133
|
+
* "ref_entity": "order",
|
|
134
|
+
* "ref_seq": 123,
|
|
135
|
+
* "data": {"order_seq": "123"}
|
|
136
|
+
* }
|
|
137
|
+
*/
|
|
138
|
+
public function push(): string
|
|
139
|
+
{
|
|
140
|
+
try {
|
|
141
|
+
$body = $this->es->readRequestBody($this->request);
|
|
142
|
+
} catch (\Throwable $e) {
|
|
143
|
+
return $this->response->setStatusCode(400)
|
|
144
|
+
->setJSON(['ok' => false, 'message' => $e->getMessage()])
|
|
145
|
+
->getBody();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
$target = (int) ($body[$this->pushTargetField] ?? 0);
|
|
149
|
+
if ($target <= 0) {
|
|
150
|
+
return $this->response->setStatusCode(400)
|
|
151
|
+
->setJSON([
|
|
152
|
+
'ok' => false,
|
|
153
|
+
'message' => sprintf('%s required', $this->pushTargetField),
|
|
154
|
+
])
|
|
155
|
+
->getBody();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
$title = (string) ($body['title'] ?? '');
|
|
159
|
+
$message = (string) ($body['message'] ?? $body['body'] ?? '');
|
|
160
|
+
|
|
161
|
+
$payload = [
|
|
162
|
+
$this->pushTargetField => $target,
|
|
163
|
+
'title' => $title,
|
|
164
|
+
'message' => $message,
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
if (isset($body['ref_entity'])) {
|
|
168
|
+
$payload['ref_entity'] = (string) $body['ref_entity'];
|
|
169
|
+
}
|
|
170
|
+
if (isset($body['ref_seq'])) {
|
|
171
|
+
$payload['ref_seq'] = (int) $body['ref_seq'];
|
|
172
|
+
}
|
|
173
|
+
if (isset($body['data']) && is_array($body['data'])) {
|
|
174
|
+
$payload['data'] = $body['data'];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
$result = $this->es->push($this->pushEntity, $payload);
|
|
178
|
+
return $this->response->setJSON($result)->getBody();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* GET /{entity}/push-log/list?page=1&limit=20&account_seq=1
|
|
183
|
+
*/
|
|
184
|
+
public function pushLogList(): string
|
|
185
|
+
{
|
|
186
|
+
$page = (int) ($this->request->getGet('page') ?? 1);
|
|
187
|
+
$limit = (int) ($this->request->getGet('limit') ?? 20);
|
|
188
|
+
|
|
189
|
+
$params = [
|
|
190
|
+
'page' => $page,
|
|
191
|
+
'limit' => $limit,
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
$accountSeq = (int) ($this->request->getGet('account_seq') ?? 0);
|
|
195
|
+
if ($accountSeq > 0) {
|
|
196
|
+
$params['account_seq'] = $accountSeq;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
$result = $this->es->pushLogList($params);
|
|
200
|
+
return $this->response->setJSON($result)->getBody();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -2,81 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
namespace App\Controllers;
|
|
4
4
|
|
|
5
|
-
use App\Controllers\BaseController;
|
|
6
|
-
use App\Libraries\EntityServer;
|
|
7
|
-
|
|
8
5
|
/**
|
|
9
|
-
*
|
|
6
|
+
* Product 전용 컨트롤러 예시.
|
|
7
|
+
* 기본 CRUD는 EntityController를 상속받아 그대로 사용하고,
|
|
8
|
+
* 확장 기능(order)만 이 컨트롤러에서 구현합니다.
|
|
10
9
|
*/
|
|
11
|
-
class ProductController extends
|
|
10
|
+
class ProductController extends EntityController
|
|
12
11
|
{
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
public function __construct()
|
|
16
|
-
{
|
|
17
|
-
$this->es = new EntityServer();
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** GET /products */
|
|
21
|
-
public function index(): string
|
|
22
|
-
{
|
|
23
|
-
$page = (int) ($this->request->getGet('page') ?? 1);
|
|
24
|
-
$limit = (int) ($this->request->getGet('limit') ?? 20);
|
|
25
|
-
$result = $this->es->list('product', ['page' => $page, 'limit' => $limit]);
|
|
26
|
-
|
|
27
|
-
return $this->response->setJSON($result)->getBody();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** GET /products/(:num) */
|
|
31
|
-
public function show(int $seq): string
|
|
32
|
-
{
|
|
33
|
-
$result = $this->es->get('product', $seq);
|
|
34
|
-
return $this->response->setJSON($result)->getBody();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** POST /products/search */
|
|
38
|
-
public function search(): string
|
|
39
|
-
{
|
|
40
|
-
$body = $this->request->getJSON(true);
|
|
41
|
-
$filter = $body['filter'] ?? [];
|
|
42
|
-
$params = ['page' => $body['page'] ?? 1, 'limit' => $body['limit'] ?? 20];
|
|
43
|
-
|
|
44
|
-
// 필터 예: [['field' => 'category', 'op' => 'eq', 'value' => 'electronics']]
|
|
45
|
-
$result = $this->es->query('product', $filter, $params);
|
|
46
|
-
return $this->response->setJSON($result)->getBody();
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** POST /products */
|
|
50
|
-
public function create(): string
|
|
51
|
-
{
|
|
52
|
-
$data = $this->request->getJSON(true);
|
|
53
|
-
// seq 없이 submit → 생성
|
|
54
|
-
$result = $this->es->submit('product', $data);
|
|
55
|
-
return $this->response->setStatusCode(201)->setJSON($result)->getBody();
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/** PUT /products/(:num) */
|
|
59
|
-
public function update(int $seq): string
|
|
60
|
-
{
|
|
61
|
-
$data = $this->request->getJSON(true);
|
|
62
|
-
$data['seq'] = $seq; // seq 포함 → 수정
|
|
63
|
-
$result = $this->es->submit('product', $data);
|
|
64
|
-
return $this->response->setJSON($result)->getBody();
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** DELETE /products/(:num) */
|
|
68
|
-
public function delete(int $seq): string
|
|
69
|
-
{
|
|
70
|
-
$result = $this->es->delete('product', $seq);
|
|
71
|
-
return $this->response->setJSON($result)->getBody();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** GET /products/(:num)/history */
|
|
75
|
-
public function history(int $seq): string
|
|
76
|
-
{
|
|
77
|
-
$result = $this->es->history('product', $seq);
|
|
78
|
-
return $this->response->setJSON($result)->getBody();
|
|
79
|
-
}
|
|
12
|
+
protected string $entity = 'product';
|
|
80
13
|
|
|
81
14
|
/**
|
|
82
15
|
* POST /products/order
|
|
@@ -90,7 +23,14 @@ class ProductController extends BaseController
|
|
|
90
23
|
*/
|
|
91
24
|
public function order(): string
|
|
92
25
|
{
|
|
93
|
-
|
|
26
|
+
try {
|
|
27
|
+
$body = $this->es->readRequestBody($this->request);
|
|
28
|
+
} catch (\Throwable $e) {
|
|
29
|
+
return $this->response->setStatusCode(400)
|
|
30
|
+
->setJSON(['ok' => false, 'message' => $e->getMessage()])
|
|
31
|
+
->getBody();
|
|
32
|
+
}
|
|
33
|
+
|
|
94
34
|
$productSeq = (int) ($body['product_seq'] ?? 0);
|
|
95
35
|
$qty = (int) ($body['qty'] ?? 1);
|
|
96
36
|
$buyer = $body['buyer'] ?? '';
|
|
@@ -101,16 +41,16 @@ class ProductController extends BaseController
|
|
|
101
41
|
->getBody();
|
|
102
42
|
}
|
|
103
43
|
|
|
104
|
-
$this->es->transStart(); // 서버 큐 등록, 이후 submit / delete
|
|
44
|
+
$this->es->transStart(); // 서버 큐 등록, 이후 submit / delete 는 큐에 적재
|
|
105
45
|
|
|
106
46
|
try {
|
|
107
47
|
// 1) 상품 조회 후 재고 차감
|
|
108
|
-
$product = $this->es->get(
|
|
48
|
+
$product = $this->es->get($this->entity, $productSeq);
|
|
109
49
|
$stock = (int) ($product['data']['stock'] ?? 0);
|
|
110
50
|
if ($stock < $qty) {
|
|
111
51
|
throw new \RuntimeException('재고 부족');
|
|
112
52
|
}
|
|
113
|
-
$this->es->submit(
|
|
53
|
+
$this->es->submit($this->entity, [
|
|
114
54
|
'seq' => $productSeq,
|
|
115
55
|
'stock' => $stock - $qty,
|
|
116
56
|
]);
|