create-entity-server 0.0.9 → 0.0.23

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 (56) hide show
  1. package/bin/create.js +26 -8
  2. package/package.json +1 -1
  3. package/template/.env.example +20 -3
  4. package/template/configs/database.json +173 -10
  5. package/template/configs/jwt.json +1 -0
  6. package/template/configs/oauth.json +37 -0
  7. package/template/configs/push.json +26 -0
  8. package/template/entities/Account/account_audit.json +4 -5
  9. package/template/entities/README.md +4 -4
  10. package/template/entities/{Auth → System/Auth}/account.json +0 -14
  11. package/template/entities/System/system_audit_log.json +14 -8
  12. package/template/samples/README.md +43 -21
  13. package/template/samples/browser/entity-server-client.js +453 -0
  14. package/template/samples/browser/example.html +498 -0
  15. package/template/samples/entities/01_basic_fields.json +39 -0
  16. package/template/samples/entities/02_types_and_defaults.json +67 -0
  17. package/template/samples/entities/03_hash_and_unique.json +33 -0
  18. package/template/samples/entities/04_fk_and_composite_unique.json +29 -0
  19. package/template/samples/entities/05_cache.json +55 -0
  20. package/template/samples/entities/06_history_and_hard_delete.json +60 -0
  21. package/template/samples/entities/07_license_scope.json +52 -0
  22. package/template/samples/entities/08_hook_sql.json +52 -0
  23. package/template/samples/entities/09_hook_entity.json +65 -0
  24. package/template/samples/entities/10_hook_submit_delete.json +78 -0
  25. package/template/samples/entities/11_hook_webhook.json +84 -0
  26. package/template/samples/entities/12_hook_push.json +73 -0
  27. package/template/samples/entities/13_read_only.json +54 -0
  28. package/template/samples/entities/14_optimistic_lock.json +29 -0
  29. package/template/samples/entities/15_reset_defaults.json +94 -0
  30. package/template/samples/entities/16_isolated_license.json +62 -0
  31. package/template/samples/entities/README.md +91 -0
  32. package/template/samples/flutter/lib/entity_server_client.dart +261 -48
  33. package/template/samples/java/EntityServerClient.java +325 -61
  34. package/template/samples/java/EntityServerExample.java +4 -3
  35. package/template/samples/kotlin/EntityServerClient.kt +261 -45
  36. package/template/samples/node/src/EntityServerClient.js +348 -59
  37. package/template/samples/node/src/example.js +9 -9
  38. package/template/samples/php/ci4/Config/EntityServer.php +14 -0
  39. package/template/samples/php/ci4/Controllers/EntityController.php +202 -0
  40. package/template/samples/php/ci4/Controllers/ProductController.php +16 -76
  41. package/template/samples/php/ci4/Libraries/EntityServer.php +352 -60
  42. package/template/samples/php/laravel/Services/EntityServerService.php +245 -40
  43. package/template/samples/python/entity_server.py +287 -68
  44. package/template/samples/python/example.py +7 -6
  45. package/template/samples/react/src/example.tsx +41 -25
  46. package/template/samples/swift/EntityServerClient.swift +248 -37
  47. package/template/scripts/normalize-entities.sh +10 -10
  48. package/template/scripts/run.ps1 +12 -3
  49. package/template/scripts/run.sh +120 -37
  50. package/template/scripts/update-server.ps1 +160 -4
  51. package/template/scripts/update-server.sh +132 -4
  52. package/template/samples/react/src/api/entityServerClient.ts +0 -290
  53. package/template/samples/react/src/hooks/useEntity.ts +0 -105
  54. /package/template/entities/{Auth → System/Auth}/api_keys.json +0 -0
  55. /package/template/entities/{Auth → System/Auth}/license.json +0 -0
  56. /package/template/entities/{Auth → System/Auth}/rbac_roles.json +0 -0
@@ -8,8 +8,7 @@
8
8
  * let client = EntityServerClient(
9
9
  * baseUrl: "http://your-server:47200",
10
10
  * apiKey: "your-api-key",
11
- * hmacSecret: "your-hmac-secret",
12
- * magicLen: 4 // 서버 packet_magic_len 과 동일
11
+ * hmacSecret: "your-hmac-secret"
13
12
  * )
14
13
  * let result = try await client.list("product")
15
14
  *
@@ -33,7 +32,8 @@ public final class EntityServerClient {
33
32
  private let baseURL: URL
34
33
  private let apiKey: String
35
34
  private let hmacSecret: String
36
- private let magicLen: Int
35
+ private var token: String
36
+ private let encryptRequests: Bool
37
37
  private let session: URLSession
38
38
  private var activeTxId: String? = nil
39
39
 
@@ -41,33 +41,77 @@ public final class EntityServerClient {
41
41
  baseUrl: String = "http://localhost:47200",
42
42
  apiKey: String = "",
43
43
  hmacSecret: String = "",
44
- magicLen: Int = 4,
44
+ token: String = "",
45
+ /// true 이면 POST 요청 바디를 XChaCha20-Poly1305로 암호화합니다.
46
+ encryptRequests: Bool = false,
45
47
  session: URLSession = .shared
46
48
  ) {
47
49
  self.baseURL = URL(string: baseUrl.removingSuffix("/"))!
48
50
  self.apiKey = apiKey
49
51
  self.hmacSecret = hmacSecret
50
- self.magicLen = magicLen
52
+ self.token = token
53
+ self.encryptRequests = encryptRequests
51
54
  self.session = session
52
55
  }
53
56
 
57
+ /** JWT Bearer 토큰을 설정합니다. HMAC 모드와 배타적으로 사용합니다. */
58
+ public func setToken(_ newToken: String) { token = newToken }
59
+
54
60
  // ─── CRUD ─────────────────────────────────────────────────────────
55
61
 
56
- public func get(entity: String, seq: Int64) async throws -> [String: Any] {
57
- try await request(method: "GET", path: "/v1/entity/\(entity)/\(seq)")
62
+ public func get(entity: String, seq: Int64, skipHooks: Bool = false) async throws -> [String: Any] {
63
+ let q = skipHooks ? "?skipHooks=true" : ""
64
+ return try await request(method: "GET", path: "/v1/entity/\(entity)/\(seq)\(q)")
65
+ }
66
+
67
+ /// 조건으로 단건 조회 (POST + conditions body)
68
+ ///
69
+ /// `conditions` 는 index/hash/unique 필드에만 사용 가능합니다.
70
+ /// 조건에 맞는 행이 없으면 404 오류가 발생합니다.
71
+ public func find(entity: String, conditions: [String: Any], skipHooks: Bool = false) async throws -> [String: Any] {
72
+ let q = skipHooks ? "?skipHooks=true" : ""
73
+ let body = try JSONSerialization.data(withJSONObject: conditions)
74
+ return try await request(method: "POST", path: "/v1/entity/\(entity)/find\(q)", body: body)
58
75
  }
59
76
 
60
- public func list(entity: String, page: Int = 1, limit: Int = 20) async throws -> [String: Any] {
61
- try await request(method: "GET", path: "/v1/entity/\(entity)/list?page=\(page)&limit=\(limit)")
77
+ /// 목록 조회 (POST + conditions body)
78
+ ///
79
+ /// `fields`를 미지정하면 기본적으로 인덱스 필드만 반환합니다 (가장 빠름).
80
+ /// 전체 필드 반환이 필요하면 `fields: ["*"]` 를 지정하세요.
81
+ /// `conditions` 는 index/hash/unique 필드에만 사용 가능합니다.
82
+ public func list(
83
+ entity: String,
84
+ page: Int = 1,
85
+ limit: Int = 20,
86
+ orderBy: String? = nil,
87
+ fields: [String]? = nil,
88
+ conditions: [String: Any]? = nil
89
+ ) async throws -> [String: Any] {
90
+ var qParts = ["page=\(page)", "limit=\(limit)"]
91
+ if let orderBy = orderBy { qParts.append("order_by=\(orderBy)") }
92
+ if let fields = fields, !fields.isEmpty { qParts.append("fields=\(fields.joined(separator: ","))") }
93
+ let body = try JSONSerialization.data(withJSONObject: conditions ?? [:])
94
+ return try await request(method: "POST", path: "/v1/entity/\(entity)/list?\(qParts.joined(separator: "&"))", body: body)
62
95
  }
63
96
 
64
- public func count(entity: String) async throws -> [String: Any] {
65
- try await request(method: "GET", path: "/v1/entity/\(entity)/count")
97
+ /// 건수 조회
98
+ public func count(entity: String, conditions: [String: Any]? = nil) async throws -> [String: Any] {
99
+ let body = try JSONSerialization.data(withJSONObject: conditions ?? [:])
100
+ return try await request(method: "POST", path: "/v1/entity/\(entity)/count", body: body)
66
101
  }
67
102
 
68
- public func query(entity: String, filter: [[String: Any]], page: Int = 1, limit: Int = 20) async throws -> [String: Any] {
69
- let body = try JSONSerialization.data(withJSONObject: filter)
70
- return try await request(method: "POST", path: "/v1/entity/\(entity)/query?page=\(page)&limit=\(limit)", body: body)
103
+ /// 커스텀 SQL 조회 (SELECT 전용, 인덱스 테이블만, JOIN 지원)
104
+ /// 사용자 입력은 반드시 `params` 로 바인딩하세요 (SQL Injection 방지).
105
+ public func query(
106
+ entity: String,
107
+ sql: String,
108
+ params: [Any]? = nil,
109
+ limit: Int? = nil
110
+ ) async throws -> [String: Any] {
111
+ var bodyDict: [String: Any] = ["sql": sql, "params": params ?? []]
112
+ if let limit = limit { bodyDict["limit"] = limit }
113
+ let body = try JSONSerialization.data(withJSONObject: bodyDict)
114
+ return try await request(method: "POST", path: "/v1/entity/\(entity)/query", body: body)
71
115
  }
72
116
 
73
117
  /// 트랜잭션 시작 — 서버에 트랜잭션 큐를 등록하고 transaction_id 를 반환합니다.
@@ -104,18 +148,23 @@ public final class EntityServerClient {
104
148
  return try await request(method: "POST", path: "/v1/transaction/commit/\(txId)")
105
149
  }
106
150
 
107
- public func submit(entity: String, data: [String: Any], transactionId: String? = nil) async throws -> [String: Any] {
151
+ public func submit(entity: String, data: [String: Any], transactionId: String? = nil, skipHooks: Bool = false) async throws -> [String: Any] {
108
152
  let body = try JSONSerialization.data(withJSONObject: data)
109
153
  var extra: [String: String] = [:]
110
154
  if let txId = transactionId ?? activeTxId { extra["X-Transaction-ID"] = txId }
111
- return try await request(method: "POST", path: "/v1/entity/\(entity)/submit", body: body, extraHeaders: extra)
155
+ let q = skipHooks ? "?skipHooks=true" : ""
156
+ return try await request(method: "POST", path: "/v1/entity/\(entity)/submit\(q)", body: body, extraHeaders: extra)
112
157
  }
113
158
 
114
- public func delete(entity: String, seq: Int64, transactionId: String? = nil, hard: Bool = false) async throws -> [String: Any] {
115
- let q = hard ? "?hard=true" : ""
159
+ /// 삭제. 서버는 POST /delete/:seq 로만 처리합니다.
160
+ public func delete(entity: String, seq: Int64, transactionId: String? = nil, hard: Bool = false, skipHooks: Bool = false) async throws -> [String: Any] {
161
+ var qParts: [String] = []
162
+ if hard { qParts.append("hard=true") }
163
+ if skipHooks { qParts.append("skipHooks=true") }
164
+ let q = qParts.isEmpty ? "" : "?" + qParts.joined(separator: "&")
116
165
  var extra: [String: String] = [:]
117
166
  if let txId = transactionId ?? activeTxId { extra["X-Transaction-ID"] = txId }
118
- return try await request(method: "DELETE", path: "/v1/entity/\(entity)/delete/\(seq)\(q)", extraHeaders: extra)
167
+ return try await request(method: "POST", path: "/v1/entity/\(entity)/delete/\(seq)\(q)", extraHeaders: extra)
119
168
  }
120
169
 
121
170
  public func history(entity: String, seq: Int64, page: Int = 1, limit: Int = 50) async throws -> [String: Any] {
@@ -126,6 +175,111 @@ public final class EntityServerClient {
126
175
  try await request(method: "POST", path: "/v1/entity/\(entity)/rollback/\(historySeq)")
127
176
  }
128
177
 
178
+ /// 푸시 발송 트리거 엔티티에 submit합니다.
179
+ public func push(pushEntity: String, payload: [String: Any], transactionId: String? = nil) async throws -> [String: Any] {
180
+ try await submit(entity: pushEntity, data: payload, transactionId: transactionId)
181
+ }
182
+
183
+ /// push_log 목록 조회 헬퍼
184
+ public func pushLogList(page: Int = 1, limit: Int = 20) async throws -> [String: Any] {
185
+ try await list(entity: "push_log", page: page, limit: limit)
186
+ }
187
+
188
+ /// account_device 디바이스 등록/갱신 헬퍼 (push_token 단일 필드)
189
+ public func registerPushDevice(
190
+ accountSeq: Int64,
191
+ deviceId: String,
192
+ pushToken: String,
193
+ platform: String? = nil,
194
+ deviceType: String? = nil,
195
+ browser: String? = nil,
196
+ browserVersion: String? = nil,
197
+ pushEnabled: Bool = true,
198
+ transactionId: String? = nil
199
+ ) async throws -> [String: Any] {
200
+ var payload: [String: Any] = [
201
+ "id": deviceId,
202
+ "account_seq": accountSeq,
203
+ "push_token": pushToken,
204
+ "push_enabled": pushEnabled,
205
+ ]
206
+ if let platform { payload["platform"] = platform }
207
+ if let deviceType { payload["device_type"] = deviceType }
208
+ if let browser { payload["browser"] = browser }
209
+ if let browserVersion { payload["browser_version"] = browserVersion }
210
+ return try await submit(entity: "account_device", data: payload, transactionId: transactionId)
211
+ }
212
+
213
+ /// account_device.seq 기준 push_token 갱신 헬퍼
214
+ public func updatePushDeviceToken(
215
+ deviceSeq: Int64,
216
+ pushToken: String,
217
+ pushEnabled: Bool = true,
218
+ transactionId: String? = nil
219
+ ) async throws -> [String: Any] {
220
+ try await submit(
221
+ entity: "account_device",
222
+ data: [
223
+ "seq": deviceSeq,
224
+ "push_token": pushToken,
225
+ "push_enabled": pushEnabled,
226
+ ],
227
+ transactionId: transactionId
228
+ )
229
+ }
230
+
231
+ /// account_device.seq 기준 푸시 수신 비활성화 헬퍼
232
+ public func disablePushDevice(
233
+ deviceSeq: Int64,
234
+ transactionId: String? = nil
235
+ ) async throws -> [String: Any] {
236
+ try await submit(
237
+ entity: "account_device",
238
+ data: [
239
+ "seq": deviceSeq,
240
+ "push_enabled": false,
241
+ ],
242
+ transactionId: transactionId
243
+ )
244
+ }
245
+
246
+ /// 요청 본문을 읽어 JSON으로 반환합니다.
247
+ /// - application/octet-stream: 암호 패킷 복호화
248
+ /// - 그 외: 평문 JSON 파싱
249
+ public func readRequestBody(
250
+ _ rawBody: Data,
251
+ contentType: String = "application/json",
252
+ requireEncrypted: Bool = false
253
+ ) throws -> [String: Any] {
254
+ let lowered = contentType.lowercased()
255
+ let isEncrypted = lowered.contains("application/octet-stream")
256
+
257
+ if requireEncrypted && !isEncrypted {
258
+ throw NSError(
259
+ domain: "EntityServerClient",
260
+ code: -1,
261
+ userInfo: [NSLocalizedDescriptionKey: "Encrypted request required: Content-Type must be application/octet-stream"]
262
+ )
263
+ }
264
+
265
+ if isEncrypted {
266
+ if rawBody.isEmpty {
267
+ throw NSError(
268
+ domain: "EntityServerClient",
269
+ code: -1,
270
+ userInfo: [NSLocalizedDescriptionKey: "Encrypted request body is empty"]
271
+ )
272
+ }
273
+ return try decryptPacket(rawBody)
274
+ }
275
+
276
+ if rawBody.isEmpty { return [:] }
277
+ guard let json = try JSONSerialization.jsonObject(with: rawBody) as? [String: Any] else {
278
+ throw EntityServerError.invalidResponse
279
+ }
280
+ return json
281
+ }
282
+
129
283
  // ─── 내부 ─────────────────────────────────────────────────────────
130
284
 
131
285
  private func request(method: String, path: String, body: Data? = nil, extraHeaders: [String: String] = [:]) async throws -> [String: Any] {
@@ -133,25 +287,38 @@ public final class EntityServerClient {
133
287
  var req = URLRequest(url: url)
134
288
  req.httpMethod = method
135
289
 
136
- let bodyStr = body.map { String(data: $0, encoding: .utf8) ?? "" } ?? ""
137
- let timestamp = String(Int(Date().timeIntervalSince1970))
138
- let nonce = UUID().uuidString
139
- let signature = try sign(method: method, path: path, timestamp: timestamp, nonce: nonce, body: bodyStr)
290
+ // 요청 바디 결정: encryptRequests POST 바디를 암호화합니다.
291
+ var requestBody = body
292
+ var requestContentType = "application/json"
293
+ if let body = body, encryptRequests || packetEncryption {
294
+ requestBody = try encryptPacket(body)
295
+ requestContentType = "application/octet-stream"
296
+ }
140
297
 
141
- req.setValue("application/json", forHTTPHeaderField: "Content-Type")
142
- req.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
143
- req.setValue(timestamp, forHTTPHeaderField: "X-Timestamp")
144
- req.setValue(nonce, forHTTPHeaderField: "X-Nonce")
145
- req.setValue(signature, forHTTPHeaderField: "X-Signature")
298
+ let bodyForSign = requestBody ?? Data()
299
+ let isHmacMode = !apiKey.isEmpty && !hmacSecret.isEmpty
300
+
301
+ req.setValue(requestContentType, forHTTPHeaderField: "Content-Type")
302
+ if isHmacMode {
303
+ let timestamp = String(Int(Date().timeIntervalSince1970))
304
+ let nonce = UUID().uuidString
305
+ let signature = try sign(method: method, path: path, timestamp: timestamp, nonce: nonce, bodyData: bodyForSign)
306
+ req.setValue(apiKey, forHTTPHeaderField: "X-API-Key")
307
+ req.setValue(timestamp, forHTTPHeaderField: "X-Timestamp")
308
+ req.setValue(nonce, forHTTPHeaderField: "X-Nonce")
309
+ req.setValue(signature, forHTTPHeaderField: "X-Signature")
310
+ } else if !token.isEmpty {
311
+ req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
312
+ }
146
313
  extraHeaders.forEach { req.setValue($0.value, forHTTPHeaderField: $0.key) }
147
- req.httpBody = body
314
+ req.httpBody = requestBody
148
315
 
149
316
  let (data, response) = try await session.data(for: req)
150
317
  let http = response as! HTTPURLResponse
151
- let contentType = http.value(forHTTPHeaderField: "Content-Type") ?? ""
318
+ let respContentType = http.value(forHTTPHeaderField: "Content-Type") ?? ""
152
319
 
153
320
  // 패킷 암호화 응답: application/octet-stream → 복호화
154
- if contentType.contains("application/octet-stream") {
321
+ if respContentType.contains("application/octet-stream") {
155
322
  return try decryptPacket(data)
156
323
  }
157
324
 
@@ -161,14 +328,50 @@ public final class EntityServerClient {
161
328
  return json
162
329
  }
163
330
 
331
+ /**
332
+ * 패킷 암호화 키를 유도합니다.
333
+ * - HMAC 모드: HKDF-SHA256(hmac_secret, "entity-server:packet-encryption")
334
+ * - JWT 모드: SHA256(token)
335
+ */
336
+ private func derivePacketKey() throws -> [UInt8] {
337
+ if !token.isEmpty && hmacSecret.isEmpty {
338
+ return Digest.sha256(Array(token.utf8))
339
+ }
340
+ return try HKDF(
341
+ password: Array(hmacSecret.utf8),
342
+ salt: Array("entity-server:hkdf:v1".utf8),
343
+ info: Array("entity-server:packet-encryption".utf8),
344
+ keyLength: 32,
345
+ variant: .sha256
346
+ ).calculate()
347
+ }
348
+
349
+ /**
350
+ * XChaCha20-Poly1305 패킷 암호화
351
+ * 포맷: [magic:magicLen][nonce:24][ciphertext+tag]
352
+ * 키: HKDF-SHA256(hmac_secret, "entity-server:packet-encryption")
353
+ * magicLen: 2 + key[31] % 14
354
+ */
355
+ private func encryptPacket(_ data: Data) throws -> Data {
356
+ let key = try derivePacketKey()
357
+ var generator = SystemRandomNumberGenerator()
358
+ let magicLen = 2 + Int(key[31]) % 14
359
+ let magic = (0..<magicLen).map { _ in generator.next() as UInt8 }
360
+ let nonce = (0..<24).map { _ in generator.next() as UInt8 }
361
+ let xchacha = XChaCha20Poly1305(key: key, iv: nonce, aad: [])
362
+ let ciphertext = try xchacha.encrypt(Array(data))
363
+ return Data(magic + nonce + ciphertext)
364
+ }
365
+
164
366
  /**
165
367
  * XChaCha20-Poly1305 패킷 복호화
166
368
  * 포맷: [magic:magicLen][nonce:24][ciphertext+tag]
167
- * 키: sha256(hmac_secret)
369
+ * 키: HKDF-SHA256(hmac_secret, "entity-server:packet-encryption")
168
370
  */
169
371
  private func decryptPacket(_ data: Data) throws -> [String: Any] {
170
- let key = Array(SHA2(variant: .sha256).calculate(for: Array(hmacSecret.utf8)))
372
+ let key = try derivePacketKey()
171
373
  let bytes = Array(data)
374
+ let magicLen = 2 + Int(key[31]) % 14
172
375
  let nonce = Array(bytes[magicLen..<(magicLen + 24)])
173
376
  let ciphertext = Array(bytes[(magicLen + 24)...])
174
377
 
@@ -182,10 +385,11 @@ public final class EntityServerClient {
182
385
  return json
183
386
  }
184
387
 
185
- /** HMAC-SHA256 서명 */
186
- private func sign(method: String, path: String, timestamp: String, nonce: String, body: String) throws -> String {
187
- let payload = [method, path, timestamp, nonce, body].joined(separator: "|")
188
- let mac = try HMAC(key: Array(hmacSecret.utf8), variant: .sha256).authenticate(Array(payload.utf8))
388
+ /** HMAC-SHA256 서명. bodyData 는 JSON 또는 암호화된 바이너리 모두 지원합니다. */
389
+ private func sign(method: String, path: String, timestamp: String, nonce: String, bodyData: Data) throws -> String {
390
+ var payload = "\(method)|\(path)|\(timestamp)|\(nonce)|".data(using: .utf8)!
391
+ payload.append(bodyData)
392
+ let mac = try HMAC(key: Array(hmacSecret.utf8), variant: .sha256).authenticate(Array(payload))
189
393
  return mac.map { String(format: "%02x", $0) }.joined()
190
394
  }
191
395
  }
@@ -196,6 +400,13 @@ private struct XChaCha20Poly1305 {
196
400
  let iv: [UInt8]
197
401
  let aad: [UInt8]
198
402
 
403
+ func encrypt(_ plaintext: [UInt8]) throws -> [UInt8] {
404
+ let chacha = try ChaCha20(key: key, iv: iv)
405
+ let ct = try chacha.encrypt(plaintext)
406
+ let poly = try Poly1305(key: chacha.keystream().prefix(32)).authenticate(ct + aad)
407
+ return ct + poly
408
+ }
409
+
199
410
  func decrypt(_ ciphertext: [UInt8]) throws -> [UInt8] {
200
411
  // CryptoSwift ChaCha20.Poly1305 AEAD - tag는 마지막 16바이트
201
412
  let tag = Array(ciphertext.suffix(16))
@@ -20,7 +20,7 @@ if [ $# -eq 0 ]; then
20
20
  echo "====================="
21
21
  echo ""
22
22
  echo "Remove redundant default values and reorder keys in entity JSON files."
23
- echo "Also auto-creates missing required entities (api_keys, rbac_roles, and account/user when JWT is enabled)."
23
+ echo "Also auto-creates missing required entities (api_keys, rbac_roles, and account/account_login_log when JWT is enabled)."
24
24
  echo ""
25
25
  echo "Usage: $0 [options]"
26
26
  echo ""
@@ -41,10 +41,10 @@ if [ $# -eq 0 ]; then
41
41
  echo " - Reorder top-level keys to canonical order"
42
42
  echo ""
43
43
  echo "Required entities (auto-created if missing, full mode only):"
44
- echo " - api_keys → entities/Auth/api_keys.json"
45
- echo " - rbac_roles → entities/Auth/rbac_roles.json"
46
- echo " - account → entities/Auth/account.json (JWT enabled only)"
47
- echo " - user → entities/User/user.json (JWT enabled only)"
44
+ echo " - api_keys → entities/System/Auth/api_keys.json"
45
+ echo " - rbac_roles → entities/System/Auth/rbac_roles.json"
46
+ echo " - account → entities/System/Auth/account.json (JWT enabled only)"
47
+ echo " - account_login_log → entities/System/Auth/account_login_log.json (JWT enabled only)"
48
48
  echo ""
49
49
  echo "Examples:"
50
50
  echo " $0 # Dry-run all entities"
@@ -56,7 +56,7 @@ if [ $# -eq 0 ]; then
56
56
  echo "=================="
57
57
  echo ""
58
58
  echo "엔티티 JSON 파일에서 불필요한 기본값을 제거하고 키 순서를 정렬합니다."
59
- echo "전체 모드에서는 필수 엔티티(api_keys, rbac_roles, JWT 사용 시 account/user)가 없으면 자동 생성합니다."
59
+ echo "전체 모드에서는 필수 엔티티(api_keys, rbac_roles, JWT 사용 시 account/account_login_log)가 없으면 자동 생성합니다."
60
60
  echo ""
61
61
  echo "사용법: $0 [옵션]"
62
62
  echo ""
@@ -77,10 +77,10 @@ if [ $# -eq 0 ]; then
77
77
  echo " - 최상위 키 순서 정규화"
78
78
  echo ""
79
79
  echo "필수 엔티티 자동 생성 (전체 모드, 없을 경우):"
80
- echo " - api_keys → entities/Auth/api_keys.json"
81
- echo " - rbac_roles → entities/Auth/rbac_roles.json"
82
- echo " - account → entities/Auth/account.json (JWT 활성 시)"
83
- echo " - user → entities/User/user.json (JWT 활성 시)"
80
+ echo " - api_keys → entities/System/Auth/api_keys.json"
81
+ echo " - rbac_roles → entities/System/Auth/rbac_roles.json"
82
+ echo " - account → entities/System/Auth/account.json (JWT 활성 시)"
83
+ echo " - account_login_log → entities/System/Auth/account_login_log.json (JWT 활성 시)"
84
84
  echo ""
85
85
  echo "예제:"
86
86
  echo " $0 # 전체 엔티티 dry-run 미리보기"
@@ -83,6 +83,10 @@ function Stop-Server {
83
83
 
84
84
  function Show-Status {
85
85
  $ServerBin = Join-Path $ProjectRoot "bin\entity-server.exe"
86
+ if (-not (Test-Path $ServerBin)) {
87
+ $LegacyBin = Join-Path $ProjectRoot "entity-server.exe"
88
+ if (Test-Path $LegacyBin) { $ServerBin = $LegacyBin }
89
+ }
86
90
  if (Is-Running) {
87
91
  & $ServerBin banner-status RUNNING
88
92
  if ($Language -eq "en") { Write-Host "Stop: .\run.ps1 stop" }
@@ -149,9 +153,14 @@ if (-not (Test-Path $DatabaseConfig)) {
149
153
  }
150
154
  $ServerBin = Join-Path $ProjectRoot "bin\entity-server.exe"
151
155
  if (-not (Test-Path $ServerBin)) {
152
- if ($Language -eq "en") { Write-Host "X bin/entity-server.exe not found" }
153
- else { Write-Host "X bin/entity-server.exe 파일이 없습니다" }
154
- exit 1
156
+ $LegacyBin = Join-Path $ProjectRoot "entity-server.exe"
157
+ if (Test-Path $LegacyBin) {
158
+ $ServerBin = $LegacyBin
159
+ } else {
160
+ if ($Language -eq "en") { Write-Host "X entity-server.exe not found (bin/entity-server.exe or .\entity-server.exe)" }
161
+ else { Write-Host "X entity-server.exe 파일이 없습니다 (bin/entity-server.exe 또는 .\entity-server.exe)" }
162
+ exit 1
163
+ }
155
164
  }
156
165
 
157
166
  function Update-JsonField {