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.
Files changed (46) hide show
  1. package/bin/create.js +11 -1
  2. package/package.json +1 -1
  3. package/template/.env.example +17 -1
  4. package/template/configs/jwt.json +1 -0
  5. package/template/configs/oauth.json +37 -0
  6. package/template/configs/push.json +26 -0
  7. package/template/entities/README.md +4 -4
  8. package/template/entities/{Auth → System/Auth}/account.json +0 -14
  9. package/template/samples/README.md +16 -0
  10. package/template/samples/entities/01_basic_fields.json +39 -0
  11. package/template/samples/entities/02_types_and_defaults.json +68 -0
  12. package/template/samples/entities/03_hash_and_unique.json +33 -0
  13. package/template/samples/entities/04_fk_and_composite_unique.json +31 -0
  14. package/template/samples/entities/05_cache.json +54 -0
  15. package/template/samples/entities/06_history_and_hard_delete.json +42 -0
  16. package/template/samples/entities/07_license_scope.json +43 -0
  17. package/template/samples/entities/08_hook_sql.json +52 -0
  18. package/template/samples/entities/09_hook_entity.json +71 -0
  19. package/template/samples/entities/10_hook_submit_delete.json +75 -0
  20. package/template/samples/entities/11_hook_webhook.json +82 -0
  21. package/template/samples/entities/12_hook_push.json +73 -0
  22. package/template/samples/entities/13_read_only.json +51 -0
  23. package/template/samples/entities/14_optimistic_lock.json +29 -0
  24. package/template/samples/entities/15_reset_defaults.json +95 -0
  25. package/template/samples/entities/README.md +94 -0
  26. package/template/samples/entities/order_notification.json +51 -0
  27. package/template/samples/flutter/lib/entity_server_client.dart +91 -0
  28. package/template/samples/java/EntityServerClient.java +117 -0
  29. package/template/samples/kotlin/EntityServerClient.kt +86 -0
  30. package/template/samples/node/src/EntityServerClient.js +116 -0
  31. package/template/samples/php/ci4/Config/EntityServer.php +15 -0
  32. package/template/samples/php/ci4/Controllers/EntityController.php +202 -0
  33. package/template/samples/php/ci4/Controllers/ProductController.php +16 -76
  34. package/template/samples/php/ci4/Libraries/EntityServer.php +150 -11
  35. package/template/samples/php/laravel/Services/EntityServerService.php +56 -0
  36. package/template/samples/python/entity_server.py +106 -0
  37. package/template/samples/react/src/api/entityServerClient.ts +123 -0
  38. package/template/samples/react/src/hooks/useEntity.ts +68 -0
  39. package/template/samples/swift/EntityServerClient.swift +105 -0
  40. package/template/scripts/normalize-entities.sh +10 -10
  41. package/template/scripts/run.sh +108 -29
  42. package/template/scripts/update-server.ps1 +92 -2
  43. package/template/scripts/update-server.sh +73 -2
  44. /package/template/entities/{Auth → System/Auth}/api_keys.json +0 -0
  45. /package/template/entities/{Auth → System/Auth}/license.json +0 -0
  46. /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
- * EntityServer 라이브러리를 사용하는 CI4 컨트롤러 예시
6
+ * Product 전용 컨트롤러 예시.
7
+ * 기본 CRUD는 EntityController를 상속받아 그대로 사용하고,
8
+ * 확장 기능(order)만 이 컨트롤러에서 구현합니다.
10
9
  */
11
- class ProductController extends BaseController
10
+ class ProductController extends EntityController
12
11
  {
13
- private EntityServer $es;
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
- $body = $this->request->getJSON(true);
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('product', $productSeq);
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('product', [
53
+ $this->es->submit($this->entity, [
114
54
  'seq' => $productSeq,
115
55
  'stock' => $stock - $qty,
116
56
  ]);