create-entity-app-server 0.0.3 → 0.0.4

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 (731) hide show
  1. package/bin/create.js +47 -13
  2. package/dist/.env.example +68 -0
  3. package/dist/app/plugins/ocr/cache.ts +1 -0
  4. package/dist/app/plugins/ocr/config.ts +1 -0
  5. package/dist/app/plugins/ocr/direction.ts +1 -0
  6. package/dist/app/plugins/ocr/dispatch.ts +1 -0
  7. package/dist/app/plugins/ocr/entity-adapter.ts +1 -0
  8. package/dist/app/plugins/ocr/errors.ts +1 -0
  9. package/dist/app/plugins/ocr/handlers.ts +1 -0
  10. package/dist/app/plugins/ocr/index.ts +1 -0
  11. package/dist/app/plugins/ocr/llm-parser.ts +1 -0
  12. package/dist/app/plugins/ocr/parsing-pipeline.ts +1 -0
  13. package/dist/app/plugins/ocr/pdf-converter.ts +1 -0
  14. package/dist/app/plugins/ocr/preprocessor.ts +1 -0
  15. package/dist/app/plugins/ocr/providers/aws.ts +1 -0
  16. package/dist/app/plugins/ocr/providers/azure.ts +1 -0
  17. package/dist/app/plugins/ocr/providers/google.ts +1 -0
  18. package/dist/app/plugins/ocr/providers/index.ts +1 -0
  19. package/dist/app/plugins/ocr/providers/naver.ts +1 -0
  20. package/dist/app/plugins/ocr/providers/tesseract.ts +1 -0
  21. package/dist/app/plugins/ocr/providers/upstage.ts +1 -0
  22. package/dist/app/plugins/ocr/quota.ts +1 -0
  23. package/dist/app/plugins/ocr/refiner.ts +1 -0
  24. package/dist/app/plugins/ocr/routes.ts +1 -0
  25. package/dist/app/plugins/ocr/service.ts +1 -0
  26. package/dist/app/plugins/ocr/template-loader.ts +1 -0
  27. package/dist/app/plugins/ocr/template-matcher.ts +1 -0
  28. package/dist/app/plugins/ocr/types/config.ts +1 -0
  29. package/dist/app/plugins/ocr/types/driver.ts +1 -0
  30. package/dist/app/plugins/ocr/types/index.ts +1 -0
  31. package/dist/app/plugins/ocr/types/parsed.ts +1 -0
  32. package/dist/app/plugins/ocr/types/store.ts +1 -0
  33. package/dist/app/plugins/ocr/types/template.ts +1 -0
  34. package/dist/app/plugins/ocr/utils.ts +1 -0
  35. package/dist/app/plugins/sms/config.ts +1 -0
  36. package/dist/app/plugins/sms/entity-adapter.ts +1 -0
  37. package/dist/app/plugins/sms/handlers.ts +1 -0
  38. package/dist/app/plugins/sms/index.ts +1 -0
  39. package/dist/app/plugins/sms/providers/aligo.ts +1 -0
  40. package/dist/app/plugins/sms/providers/aws-sns.ts +1 -0
  41. package/dist/app/plugins/sms/providers/index.ts +1 -0
  42. package/dist/app/plugins/sms/providers/nhn.ts +1 -0
  43. package/dist/app/plugins/sms/providers/ppurio.ts +1 -0
  44. package/dist/app/plugins/sms/providers/solapi.ts +1 -0
  45. package/dist/app/plugins/sms/routes.ts +1 -0
  46. package/dist/app/plugins/sms/service.ts +1 -0
  47. package/dist/app/plugins/sms/types/client.ts +1 -0
  48. package/dist/app/plugins/sms/types/config.ts +1 -0
  49. package/dist/app/plugins/sms/types/index.ts +1 -0
  50. package/dist/app/plugins/sms/types/job.ts +1 -0
  51. package/dist/app/plugins/sms/verification.ts +1 -0
  52. package/dist/app/plugins/smtp/config.ts +1 -0
  53. package/dist/app/plugins/smtp/handlers.ts +1 -0
  54. package/dist/app/plugins/smtp/index.ts +1 -0
  55. package/dist/app/plugins/smtp/routes.ts +1 -0
  56. package/dist/app/plugins/smtp/types/config.ts +1 -0
  57. package/dist/app/plugins/smtp/types/index.ts +1 -0
  58. package/dist/logs/access.1.log +0 -0
  59. package/dist/system-api.js +1 -0
  60. package/dist/system.js +1 -0
  61. package/package.json +3 -8
  62. package/docs/design/board-api-design.md +0 -2342
  63. package/scripts/_gen-table-type.ts +0 -605
  64. package/scripts/build-minify-plugins.mjs +0 -124
  65. package/scripts/build-obfuscate-system.mjs +0 -38
  66. package/scripts/build.sh +0 -140
  67. package/scripts/gen-table-type.sh +0 -169
  68. package/scripts/push.sh +0 -102
  69. package/scripts/release.sh +0 -51
  70. package/src/app/plugins/2fa/config.json +0 -17
  71. package/src/app/plugins/ais/config.json +0 -7
  72. package/src/app/plugins/ais/config.ts +0 -32
  73. package/src/app/plugins/ais/docs/README.md +0 -142
  74. package/src/app/plugins/ais/docs/api.md +0 -138
  75. package/src/app/plugins/ais/entities/ais_vessel.json +0 -64
  76. package/src/app/plugins/ais/handlers.ts +0 -88
  77. package/src/app/plugins/ais/index.ts +0 -21
  78. package/src/app/plugins/ais/routes.ts +0 -13
  79. package/src/app/plugins/ais/service.ts +0 -242
  80. package/src/app/plugins/ais/types/index.ts +0 -78
  81. package/src/app/plugins/alimtalk/config.json +0 -26
  82. package/src/app/plugins/distance-server/config.json +0 -6
  83. package/src/app/plugins/distance-server/config.ts +0 -50
  84. package/src/app/plugins/distance-server/docs/README.md +0 -114
  85. package/src/app/plugins/distance-server/handlers.ts +0 -104
  86. package/src/app/plugins/distance-server/index.ts +0 -23
  87. package/src/app/plugins/distance-server/routes.ts +0 -36
  88. package/src/app/plugins/distance-server/service.ts +0 -187
  89. package/src/app/plugins/distance-server/types/index.ts +0 -8
  90. package/src/app/plugins/friendtalk/config.json +0 -11
  91. package/src/app/plugins/holidays/config.json +0 -10
  92. package/src/app/plugins/identity/config.json +0 -30
  93. package/src/app/plugins/kobc_freight/config.json +0 -6
  94. package/src/app/plugins/kobc_freight/config.ts +0 -28
  95. package/src/app/plugins/kobc_freight/docs/README.md +0 -316
  96. package/src/app/plugins/kobc_freight/entities/kobc_freight_entry.json +0 -31
  97. package/src/app/plugins/kobc_freight/entities/kobc_kcci_entry.json +0 -67
  98. package/src/app/plugins/kobc_freight/entities/kobc_kpli_entry.json +0 -27
  99. package/src/app/plugins/kobc_freight/entities/kobc_ncfi_entry.json +0 -99
  100. package/src/app/plugins/kobc_freight/handlers.ts +0 -283
  101. package/src/app/plugins/kobc_freight/index.ts +0 -21
  102. package/src/app/plugins/kobc_freight/routes.ts +0 -39
  103. package/src/app/plugins/kobc_freight/service.ts +0 -604
  104. package/src/app/plugins/kobc_freight/types/index.ts +0 -99
  105. package/src/app/plugins/llm/config.json +0 -71
  106. package/src/app/plugins/oauth/config.json +0 -72
  107. package/src/app/plugins/ocr/cache.ts +0 -50
  108. package/src/app/plugins/ocr/config.json +0 -110
  109. package/src/app/plugins/ocr/config.ts +0 -126
  110. package/src/app/plugins/ocr/direction.ts +0 -48
  111. package/src/app/plugins/ocr/dispatch.ts +0 -130
  112. package/src/app/plugins/ocr/entity-adapter.ts +0 -198
  113. package/src/app/plugins/ocr/errors.ts +0 -42
  114. package/src/app/plugins/ocr/handlers.ts +0 -250
  115. package/src/app/plugins/ocr/index.ts +0 -68
  116. package/src/app/plugins/ocr/llm-parser.ts +0 -164
  117. package/src/app/plugins/ocr/parsing-pipeline.ts +0 -87
  118. package/src/app/plugins/ocr/pdf-converter.ts +0 -136
  119. package/src/app/plugins/ocr/preprocessor.ts +0 -313
  120. package/src/app/plugins/ocr/providers/aws.ts +0 -200
  121. package/src/app/plugins/ocr/providers/azure.ts +0 -183
  122. package/src/app/plugins/ocr/providers/google.ts +0 -155
  123. package/src/app/plugins/ocr/providers/index.ts +0 -80
  124. package/src/app/plugins/ocr/providers/naver.ts +0 -186
  125. package/src/app/plugins/ocr/providers/tesseract.ts +0 -198
  126. package/src/app/plugins/ocr/providers/upstage.ts +0 -156
  127. package/src/app/plugins/ocr/quota.ts +0 -108
  128. package/src/app/plugins/ocr/refiner.ts +0 -112
  129. package/src/app/plugins/ocr/routes.ts +0 -19
  130. package/src/app/plugins/ocr/service.ts +0 -333
  131. package/src/app/plugins/ocr/template-loader.ts +0 -72
  132. package/src/app/plugins/ocr/template-matcher.ts +0 -422
  133. package/src/app/plugins/ocr/types/config.ts +0 -60
  134. package/src/app/plugins/ocr/types/driver.ts +0 -71
  135. package/src/app/plugins/ocr/types/index.ts +0 -5
  136. package/src/app/plugins/ocr/types/parsed.ts +0 -101
  137. package/src/app/plugins/ocr/types/store.ts +0 -70
  138. package/src/app/plugins/ocr/types/template.ts +0 -89
  139. package/src/app/plugins/ocr/utils.ts +0 -18
  140. package/src/app/plugins/pg/config.json +0 -35
  141. package/src/app/plugins/push/config.json +0 -18
  142. package/src/app/plugins/sms/config.json +0 -33
  143. package/src/app/plugins/sms/config.ts +0 -158
  144. package/src/app/plugins/sms/entity-adapter.ts +0 -213
  145. package/src/app/plugins/sms/handlers.ts +0 -149
  146. package/src/app/plugins/sms/index.ts +0 -93
  147. package/src/app/plugins/sms/providers/aligo.ts +0 -73
  148. package/src/app/plugins/sms/providers/aws-sns.ts +0 -182
  149. package/src/app/plugins/sms/providers/index.ts +0 -47
  150. package/src/app/plugins/sms/providers/nhn.ts +0 -82
  151. package/src/app/plugins/sms/providers/ppurio.ts +0 -76
  152. package/src/app/plugins/sms/providers/solapi.ts +0 -83
  153. package/src/app/plugins/sms/routes.ts +0 -23
  154. package/src/app/plugins/sms/service.ts +0 -239
  155. package/src/app/plugins/sms/types/client.ts +0 -41
  156. package/src/app/plugins/sms/types/config.ts +0 -46
  157. package/src/app/plugins/sms/types/index.ts +0 -3
  158. package/src/app/plugins/sms/types/job.ts +0 -51
  159. package/src/app/plugins/sms/verification.ts +0 -162
  160. package/src/app/plugins/smtp/config.ts +0 -41
  161. package/src/app/plugins/smtp/handlers.ts +0 -52
  162. package/src/app/plugins/smtp/index.ts +0 -33
  163. package/src/app/plugins/smtp/routes.ts +0 -19
  164. package/src/app/plugins/smtp/types/config.ts +0 -8
  165. package/src/app/plugins/smtp/types/index.ts +0 -1
  166. package/src/app/plugins/taxinvoice/config.json +0 -35
  167. package/src/app/routes/calendar/config.json +0 -5
  168. package/src/app/routes/calendar/entities/calendar_attendees.json +0 -23
  169. package/src/app/routes/calendar/entities/calendar_comments.json +0 -17
  170. package/src/app/routes/calendar/entities/calendar_events.json +0 -48
  171. package/src/app/routes/calendar/entities/calendar_kind.json +0 -11
  172. package/src/app/routes/calendar/entities/calendar_method.json +0 -11
  173. package/src/app/routes/calendar/routes.ts +0 -32
  174. package/src/app/routes/email-verify/config-loader.ts +0 -47
  175. package/src/app/routes/email-verify/config.example.json +0 -13
  176. package/src/app/routes/email-verify/config.json +0 -16
  177. package/src/app/routes/email-verify/entities/account.json +0 -23
  178. package/src/app/routes/email-verify/handlers/activate.ts +0 -103
  179. package/src/app/routes/email-verify/handlers/change.ts +0 -106
  180. package/src/app/routes/email-verify/handlers/confirm.ts +0 -87
  181. package/src/app/routes/email-verify/handlers/index.ts +0 -20
  182. package/src/app/routes/email-verify/handlers/send.ts +0 -157
  183. package/src/app/routes/email-verify/handlers/status.ts +0 -53
  184. package/src/app/routes/email-verify/handlers/utils.ts +0 -85
  185. package/src/app/routes/email-verify/routes.ts +0 -54
  186. package/src/app/routes/email-verify/templates/verification.html +0 -15
  187. package/src/app/routes/email-verify/templates/verification_link.html +0 -19
  188. package/src/app/routes/email-verify/types/index.ts +0 -77
  189. package/src/app/routes/email-verify/verification-utils.ts +0 -57
  190. package/src/app/routes/example-db/config.json +0 -5
  191. package/src/app/routes/example-db/handlers.ts +0 -220
  192. package/src/app/routes/example-db/models/account-ext.ts +0 -33
  193. package/src/app/routes/example-db/models/users.ts +0 -30
  194. package/src/app/routes/example-db/routes.ts +0 -23
  195. package/src/app/routes/example-db/types/defaults.ts +0 -21
  196. package/src/app/routes/example-db/types/index.ts +0 -4
  197. package/src/app/routes/example-db/types/params.ts +0 -3
  198. package/src/app/routes/example-db/types/query.ts +0 -6
  199. package/src/app/routes/example-db/types/user.ts +0 -11
  200. package/src/app/routes/example-es/config.json +0 -5
  201. package/src/app/routes/example-es/handlers.ts +0 -216
  202. package/src/app/routes/example-es/routes.ts +0 -24
  203. package/src/app/routes/example-es/types/defaults.ts +0 -30
  204. package/src/app/routes/example-es/types/index.ts +0 -4
  205. package/src/app/routes/example-es/types/params.ts +0 -3
  206. package/src/app/routes/example-es/types/post.ts +0 -12
  207. package/src/app/routes/example-es/types/query.ts +0 -14
  208. package/src/app/routes/funeral/config.json +0 -5
  209. package/src/app/routes/funeral/entities/funeral.json +0 -77
  210. package/src/app/routes/funeral/entities/funeral_docs.json +0 -36
  211. package/src/app/routes/funeral/entities/funeral_mourner.json +0 -31
  212. package/src/app/routes/funeral/entities/funeral_order.json +0 -48
  213. package/src/app/routes/funeral/entities/funeral_room.json +0 -61
  214. package/src/app/routes/funeral/entities/funeral_schedule.json +0 -39
  215. package/src/app/routes/funeral/routes.ts +0 -32
  216. package/src/app/routes/health/config.json +0 -5
  217. package/src/app/routes/health/handlers.ts +0 -69
  218. package/src/app/routes/health/routes.ts +0 -14
  219. package/src/app/routes/hr/career/config.json +0 -5
  220. package/src/app/routes/hr/career/entities/employee_career.json +0 -15
  221. package/src/app/routes/hr/career/routes.ts +0 -25
  222. package/src/app/routes/hr/config.json +0 -5
  223. package/src/app/routes/hr/education/config.json +0 -5
  224. package/src/app/routes/hr/education/entities/employee_education.json +0 -29
  225. package/src/app/routes/hr/education/entities/employee_education_mans.json +0 -25
  226. package/src/app/routes/hr/education/entities/employee_school.json +0 -19
  227. package/src/app/routes/hr/education/routes.ts +0 -28
  228. package/src/app/routes/hr/employee/config.json +0 -5
  229. package/src/app/routes/hr/employee/entities/employee.json +0 -59
  230. package/src/app/routes/hr/employee/entities/employee_cert.json +0 -19
  231. package/src/app/routes/hr/employee/entities/employee_reward.json +0 -21
  232. package/src/app/routes/hr/employee/routes.ts +0 -27
  233. package/src/app/routes/hr/entities/hr_group.json +0 -47
  234. package/src/app/routes/hr/entities/hr_group_pv.json +0 -20
  235. package/src/app/routes/hr/entities/hr_role.json +0 -43
  236. package/src/app/routes/hr/entities/hr_role_pv.json +0 -20
  237. package/src/app/routes/hr/routes.ts +0 -29
  238. package/src/app/routes/messages/chat/config.json +0 -5
  239. package/src/app/routes/messages/chat/entities/user_chat.json +0 -47
  240. package/src/app/routes/messages/chat/entities/user_chat_room.json +0 -38
  241. package/src/app/routes/messages/chat/entities/user_chat_room_member.json +0 -49
  242. package/src/app/routes/messages/chat/routes.ts +0 -28
  243. package/src/app/routes/messages/msgbox/config.json +0 -5
  244. package/src/app/routes/messages/msgbox/entities/user_msgbox.json +0 -73
  245. package/src/app/routes/messages/msgbox/routes.ts +0 -28
  246. package/src/app/routes/vessel-tracking/config.json +0 -3
  247. package/src/app/routes/vessel-tracking/entities/tracked_vessel.json +0 -261
  248. package/src/app/routes/vessel-tracking/handlers.ts +0 -134
  249. package/src/app/routes/vessel-tracking/routes.ts +0 -25
  250. package/src/app/routes/vessel-tracking/types/index.ts +0 -5
  251. package/src/app/routes/vessel-tracking/types/vessel.ts +0 -59
  252. package/src/app/schedules/ais_sync/config.json +0 -4
  253. package/src/app/schedules/ais_sync/index.ts +0 -69
  254. package/src/app/schedules/kobc_freight_sync/config.json +0 -4
  255. package/src/app/schedules/kobc_freight_sync/index.ts +0 -94
  256. package/src/app/schedules/vessel_kr_sync/config.json +0 -4
  257. package/src/app/schedules/vessel_kr_sync/index.ts +0 -72
  258. package/src/system/app.ts +0 -129
  259. package/src/system/cache/_store-ref.ts +0 -15
  260. package/src/system/cache/config.ts +0 -61
  261. package/src/system/cache/drivers/memcached.ts +0 -135
  262. package/src/system/cache/drivers/memory.ts +0 -92
  263. package/src/system/cache/drivers/redis.ts +0 -109
  264. package/src/system/cache/index.ts +0 -43
  265. package/src/system/cache/namespaced.ts +0 -79
  266. package/src/system/cache/plugin.ts +0 -59
  267. package/src/system/cache/types.ts +0 -81
  268. package/src/system/config/config-path.ts +0 -20
  269. package/src/system/config/cors.ts +0 -49
  270. package/src/system/config/database.ts +0 -190
  271. package/src/system/config/entity-server.ts +0 -8
  272. package/src/system/config/env-substitution.ts +0 -4
  273. package/src/system/config/env.ts +0 -30
  274. package/src/system/config/json-config.ts +0 -13
  275. package/src/system/config/module-path.ts +0 -16
  276. package/src/system/config/packet-encrypt.ts +0 -80
  277. package/src/system/config/rate-limit.ts +0 -4
  278. package/src/system/config/security-loader.ts +0 -25
  279. package/src/system/config/security.ts +0 -16
  280. package/src/system/config/server.ts +0 -81
  281. package/src/system/crypto/cipher.ts +0 -117
  282. package/src/system/crypto/data-encrypt.ts +0 -174
  283. package/src/system/crypto/hash.ts +0 -24
  284. package/src/system/crypto/packet.test.ts +0 -23
  285. package/src/system/crypto/packet.ts +0 -97
  286. package/src/system/crypto/random.ts +0 -19
  287. package/src/system/email/sender.ts +0 -85
  288. package/src/system/email/template-engine.ts +0 -147
  289. package/src/system/entity-server/bootstrap.ts +0 -270
  290. package/src/system/entity-server/client.ts +0 -64
  291. package/src/system/hooks/loader.ts +0 -32
  292. package/src/system/hooks/runner.ts +0 -159
  293. package/src/system/hooks/types.ts +0 -75
  294. package/src/system/hooks/withdraw-hooks.ts +0 -42
  295. package/src/system/http/cookie.ts +0 -62
  296. package/src/system/http/response.ts +0 -16
  297. package/src/system/index.ts +0 -48
  298. package/src/system/logging/log-format.ts +0 -50
  299. package/src/system/logging/logger.ts +0 -104
  300. package/src/system/middleware/_db-ref.ts +0 -26
  301. package/src/system/middleware/_push-ref.ts +0 -28
  302. package/src/system/middleware/access-log.ts +0 -34
  303. package/src/system/middleware/auth.ts +0 -67
  304. package/src/system/middleware/csrf.ts +0 -172
  305. package/src/system/middleware/database.ts +0 -44
  306. package/src/system/middleware/error-handler.ts +0 -51
  307. package/src/system/middleware/extension-loader.ts +0 -111
  308. package/src/system/middleware/packet-encrypt.ts +0 -281
  309. package/src/system/middleware/request-id.ts +0 -18
  310. package/src/system/plugins/access-log.ts +0 -34
  311. package/src/system/plugins/packet-encrypt.ts +0 -281
  312. package/src/system/proxy/register.ts +0 -37
  313. package/src/system/public-api.ts +0 -140
  314. package/src/system/push/sender.ts +0 -131
  315. package/src/system/routes/entity-interceptor.ts +0 -327
  316. package/src/system/routes/loader.ts +0 -215
  317. package/src/system/scheduler/cron-utils.ts +0 -150
  318. package/src/system/scheduler/distributed-lock.ts +0 -141
  319. package/src/system/scheduler/schedule-loader.ts +0 -105
  320. package/src/system/security/anonymous-device-id.ts +0 -41
  321. package/src/system/security/anonymous-device.ts +0 -98
  322. package/src/system/security/anonymous-packet-token.ts +0 -23
  323. package/src/system/security/packet-bootstrap.ts +0 -16
  324. package/src/system/security/password-policy.ts +0 -191
  325. package/src/system/startup-banner.ts +0 -191
  326. package/src/system/types/fastify.d.ts +0 -53
  327. package/src/system/utils/app-path.ts +0 -31
  328. package/src/system/utils/coerce.ts +0 -28
  329. package/src/system/utils/date-prefixed-log-stream.ts +0 -176
  330. package/src/system/utils/errors.ts +0 -66
  331. package/src/system/utils/format.ts +0 -45
  332. package/src/system/utils/http-client.ts +0 -79
  333. package/src/system/utils/user-agent.ts +0 -82
  334. package/tsconfig.app.json +0 -17
  335. package/tsconfig.json +0 -39
  336. /package/{.env.example → dist/.env} +0 -0
  337. /package/{src → dist}/app/hooks/README.md +0 -0
  338. /package/{src → dist}/app/hooks/account.ts +0 -0
  339. /package/{src → dist}/app/hooks/index.ts +0 -0
  340. /package/{src → dist}/app/hooks/order.ts +0 -0
  341. /package/{src → dist}/app/hooks/post.ts +0 -0
  342. /package/{src/app/plugins/2fa/config.example.json → dist/app/plugins/2fa/config.json} +0 -0
  343. /package/{src → dist}/app/plugins/2fa/config.ts +0 -0
  344. /package/{src → dist}/app/plugins/2fa/docs/README.md +0 -0
  345. /package/{src → dist}/app/plugins/2fa/entities/account.json +0 -0
  346. /package/{src → dist}/app/plugins/2fa/handlers/disable.ts +0 -0
  347. /package/{src → dist}/app/plugins/2fa/handlers/index.ts +0 -0
  348. /package/{src → dist}/app/plugins/2fa/handlers/recovery.ts +0 -0
  349. /package/{src → dist}/app/plugins/2fa/handlers/regenerate.ts +0 -0
  350. /package/{src → dist}/app/plugins/2fa/handlers/setup-verify.ts +0 -0
  351. /package/{src → dist}/app/plugins/2fa/handlers/setup.ts +0 -0
  352. /package/{src → dist}/app/plugins/2fa/handlers/status.ts +0 -0
  353. /package/{src → dist}/app/plugins/2fa/handlers/utils.ts +0 -0
  354. /package/{src → dist}/app/plugins/2fa/handlers/verify.ts +0 -0
  355. /package/{src → dist}/app/plugins/2fa/index.ts +0 -0
  356. /package/{src → dist}/app/plugins/2fa/routes.ts +0 -0
  357. /package/{src → dist}/app/plugins/2fa/templates/auth/2fa_disabled.html +0 -0
  358. /package/{src → dist}/app/plugins/2fa/templates/auth/2fa_recovery_regenerated.html +0 -0
  359. /package/{src → dist}/app/plugins/2fa/templates/auth/2fa_setup_complete.html +0 -0
  360. /package/{src → dist}/app/plugins/2fa/totp-utils.ts +0 -0
  361. /package/{src → dist}/app/plugins/2fa/types.ts +0 -0
  362. /package/{src → dist}/app/plugins/README.md +0 -0
  363. /package/{src/app/plugins/alimtalk/config.example.json → dist/app/plugins/alimtalk/config.json} +0 -0
  364. /package/{src → dist}/app/plugins/alimtalk/config.ts +0 -0
  365. /package/{src → dist}/app/plugins/alimtalk/docs/README.md +0 -0
  366. /package/{src → dist}/app/plugins/alimtalk/entities/alimtalk_log.json +0 -0
  367. /package/{src → dist}/app/plugins/alimtalk/entities/alimtalk_msg.json +0 -0
  368. /package/{src → dist}/app/plugins/alimtalk/entity-adapter.ts +0 -0
  369. /package/{src → dist}/app/plugins/alimtalk/handlers.ts +0 -0
  370. /package/{src → dist}/app/plugins/alimtalk/index.ts +0 -0
  371. /package/{src → dist}/app/plugins/alimtalk/providers/aligo.ts +0 -0
  372. /package/{src → dist}/app/plugins/alimtalk/providers/index.ts +0 -0
  373. /package/{src → dist}/app/plugins/alimtalk/providers/nhn.ts +0 -0
  374. /package/{src → dist}/app/plugins/alimtalk/providers/ppurio.ts +0 -0
  375. /package/{src → dist}/app/plugins/alimtalk/providers/solapi.ts +0 -0
  376. /package/{src → dist}/app/plugins/alimtalk/routes.ts +0 -0
  377. /package/{src → dist}/app/plugins/alimtalk/service.ts +0 -0
  378. /package/{src → dist}/app/plugins/alimtalk/template-cache.ts +0 -0
  379. /package/{src → dist}/app/plugins/alimtalk/templates/alimtalk.json +0 -0
  380. /package/{src → dist}/app/plugins/alimtalk/types/client.ts +0 -0
  381. /package/{src → dist}/app/plugins/alimtalk/types/config.ts +0 -0
  382. /package/{src → dist}/app/plugins/alimtalk/types/friendtalk.ts +0 -0
  383. /package/{src → dist}/app/plugins/alimtalk/types/index.ts +0 -0
  384. /package/{src → dist}/app/plugins/alimtalk/types/job.ts +0 -0
  385. /package/{src → dist}/app/plugins/alimtalk/webhook.ts +0 -0
  386. /package/{src → dist}/app/plugins/example/config.json +0 -0
  387. /package/{src → dist}/app/plugins/example/config.ts +0 -0
  388. /package/{src → dist}/app/plugins/example/docs/README.md +0 -0
  389. /package/{src → dist}/app/plugins/example/entity-adapter.ts +0 -0
  390. /package/{src → dist}/app/plugins/example/handlers.ts +0 -0
  391. /package/{src → dist}/app/plugins/example/index.ts +0 -0
  392. /package/{src → dist}/app/plugins/example/routes.ts +0 -0
  393. /package/{src → dist}/app/plugins/example/service.ts +0 -0
  394. /package/{src → dist}/app/plugins/example/types/config.ts +0 -0
  395. /package/{src → dist}/app/plugins/example/types/index.ts +0 -0
  396. /package/{src/app/plugins/friendtalk/config.example.json → dist/app/plugins/friendtalk/config.json} +0 -0
  397. /package/{src → dist}/app/plugins/friendtalk/config.ts +0 -0
  398. /package/{src → dist}/app/plugins/friendtalk/docs/README.md +0 -0
  399. /package/{src → dist}/app/plugins/friendtalk/entities/friendtalk_log.json +0 -0
  400. /package/{src → dist}/app/plugins/friendtalk/entities/friendtalk_msg.json +0 -0
  401. /package/{src → dist}/app/plugins/friendtalk/entity-adapter.ts +0 -0
  402. /package/{src → dist}/app/plugins/friendtalk/handlers.ts +0 -0
  403. /package/{src → dist}/app/plugins/friendtalk/routes.ts +0 -0
  404. /package/{src → dist}/app/plugins/friendtalk/templates/friendtalk.json +0 -0
  405. /package/{src/app/plugins/holidays/config.example.json → dist/app/plugins/holidays/config.json} +0 -0
  406. /package/{src → dist}/app/plugins/holidays/config.ts +0 -0
  407. /package/{src → dist}/app/plugins/holidays/docs/README.md +0 -0
  408. /package/{src → dist}/app/plugins/holidays/entities/holiday.json +0 -0
  409. /package/{src → dist}/app/plugins/holidays/handlers.ts +0 -0
  410. /package/{src → dist}/app/plugins/holidays/index.ts +0 -0
  411. /package/{src → dist}/app/plugins/holidays/routes.ts +0 -0
  412. /package/{src → dist}/app/plugins/holidays/service.ts +0 -0
  413. /package/{src → dist}/app/plugins/holidays/types/api.ts +0 -0
  414. /package/{src → dist}/app/plugins/holidays/types/config.ts +0 -0
  415. /package/{src → dist}/app/plugins/holidays/types/index.ts +0 -0
  416. /package/{src/app/plugins/identity/config.example.json → dist/app/plugins/identity/config.json} +0 -0
  417. /package/{src → dist}/app/plugins/identity/config.ts +0 -0
  418. /package/{src → dist}/app/plugins/identity/crypto.ts +0 -0
  419. /package/{src → dist}/app/plugins/identity/docs/README.md +0 -0
  420. /package/{src → dist}/app/plugins/identity/entities/account.json +0 -0
  421. /package/{src → dist}/app/plugins/identity/entities/identity_verification.json +0 -0
  422. /package/{src → dist}/app/plugins/identity/entity-adapter.ts +0 -0
  423. /package/{src → dist}/app/plugins/identity/handlers.ts +0 -0
  424. /package/{src → dist}/app/plugins/identity/index.ts +0 -0
  425. /package/{src → dist}/app/plugins/identity/providers/danal.ts +0 -0
  426. /package/{src → dist}/app/plugins/identity/providers/index.ts +0 -0
  427. /package/{src → dist}/app/plugins/identity/providers/kmc.ts +0 -0
  428. /package/{src → dist}/app/plugins/identity/providers/nice.ts +0 -0
  429. /package/{src → dist}/app/plugins/identity/routes.ts +0 -0
  430. /package/{src → dist}/app/plugins/identity/service.ts +0 -0
  431. /package/{src → dist}/app/plugins/identity/types/config.ts +0 -0
  432. /package/{src → dist}/app/plugins/identity/types/index.ts +0 -0
  433. /package/{src → dist}/app/plugins/identity/types/verification.ts +0 -0
  434. /package/{src → dist}/app/plugins/llm/cache.ts +0 -0
  435. /package/{src → dist}/app/plugins/llm/chatbot-store.ts +0 -0
  436. /package/{src → dist}/app/plugins/llm/chunker.ts +0 -0
  437. /package/{src/app/plugins/llm/config.example.json → dist/app/plugins/llm/config.json} +0 -0
  438. /package/{src → dist}/app/plugins/llm/config.ts +0 -0
  439. /package/{src → dist}/app/plugins/llm/conversation-store.ts +0 -0
  440. /package/{src → dist}/app/plugins/llm/docs/README.md +0 -0
  441. /package/{src → dist}/app/plugins/llm/docs/api.md +0 -0
  442. /package/{src → dist}/app/plugins/llm/document-store.ts +0 -0
  443. /package/{src → dist}/app/plugins/llm/entities/llm_chatbot.json +0 -0
  444. /package/{src → dist}/app/plugins/llm/entities/llm_conversation.json +0 -0
  445. /package/{src → dist}/app/plugins/llm/entities/llm_document.json +0 -0
  446. /package/{src → dist}/app/plugins/llm/entities/llm_usage.json +0 -0
  447. /package/{src → dist}/app/plugins/llm/entities/llm_user_profile.json +0 -0
  448. /package/{src → dist}/app/plugins/llm/handlers.ts +0 -0
  449. /package/{src → dist}/app/plugins/llm/index.ts +0 -0
  450. /package/{src → dist}/app/plugins/llm/profile-store.ts +0 -0
  451. /package/{src → dist}/app/plugins/llm/providers/anthropic.ts +0 -0
  452. /package/{src → dist}/app/plugins/llm/providers/azure.ts +0 -0
  453. /package/{src → dist}/app/plugins/llm/providers/gemini.ts +0 -0
  454. /package/{src → dist}/app/plugins/llm/providers/index.ts +0 -0
  455. /package/{src → dist}/app/plugins/llm/providers/ollama.ts +0 -0
  456. /package/{src → dist}/app/plugins/llm/providers/openai.ts +0 -0
  457. /package/{src → dist}/app/plugins/llm/routes.ts +0 -0
  458. /package/{src → dist}/app/plugins/llm/service.ts +0 -0
  459. /package/{src → dist}/app/plugins/llm/template-loader.ts +0 -0
  460. /package/{src → dist}/app/plugins/llm/templates/prompts/extract_json.json +0 -0
  461. /package/{src → dist}/app/plugins/llm/templates/prompts/summarize.json +0 -0
  462. /package/{src → dist}/app/plugins/llm/templates/prompts/translate.json +0 -0
  463. /package/{src → dist}/app/plugins/llm/types/chat.ts +0 -0
  464. /package/{src → dist}/app/plugins/llm/types/chatbot.ts +0 -0
  465. /package/{src → dist}/app/plugins/llm/types/config.ts +0 -0
  466. /package/{src → dist}/app/plugins/llm/types/conversation.ts +0 -0
  467. /package/{src → dist}/app/plugins/llm/types/index.ts +0 -0
  468. /package/{src → dist}/app/plugins/llm/types/profile.ts +0 -0
  469. /package/{src → dist}/app/plugins/llm/types/store.ts +0 -0
  470. /package/{src → dist}/app/plugins/llm/types/usage.ts +0 -0
  471. /package/{src → dist}/app/plugins/llm/usage-store.ts +0 -0
  472. /package/{src → dist}/app/plugins/oauth/account/handlers/index.ts +0 -0
  473. /package/{src → dist}/app/plugins/oauth/account/handlers/link.ts +0 -0
  474. /package/{src → dist}/app/plugins/oauth/account/handlers/providers-list.ts +0 -0
  475. /package/{src → dist}/app/plugins/oauth/account/handlers/refresh.ts +0 -0
  476. /package/{src → dist}/app/plugins/oauth/account/handlers/unlink.ts +0 -0
  477. /package/{src/app/plugins/oauth/config.example.json → dist/app/plugins/oauth/config.json} +0 -0
  478. /package/{src → dist}/app/plugins/oauth/config.ts +0 -0
  479. /package/{src → dist}/app/plugins/oauth/docs/README.md +0 -0
  480. /package/{src → dist}/app/plugins/oauth/entities/account_oauth.json +0 -0
  481. /package/{src → dist}/app/plugins/oauth/handlers/callback.ts +0 -0
  482. /package/{src → dist}/app/plugins/oauth/handlers/index.ts +0 -0
  483. /package/{src → dist}/app/plugins/oauth/handlers/redirect.ts +0 -0
  484. /package/{src → dist}/app/plugins/oauth/index.ts +0 -0
  485. /package/{src → dist}/app/plugins/oauth/providers/index.ts +0 -0
  486. /package/{src → dist}/app/plugins/oauth/routes.ts +0 -0
  487. /package/{src → dist}/app/plugins/oauth/service.ts +0 -0
  488. /package/{src → dist}/app/plugins/oauth/state.ts +0 -0
  489. /package/{src → dist}/app/plugins/oauth/types/index.ts +0 -0
  490. /package/{src → dist}/app/plugins/oauth/upsert.ts +0 -0
  491. /package/{src/app/plugins/ocr/config.example.json → dist/app/plugins/ocr/config.json} +0 -0
  492. /package/{src → dist}/app/plugins/ocr/docs/README.md +0 -0
  493. /package/{src → dist}/app/plugins/ocr/docs/api.md +0 -0
  494. /package/{src → dist}/app/plugins/ocr/entities/ocr_result.json +0 -0
  495. /package/{src → dist}/app/plugins/ocr/entities/ocr_usage.json +0 -0
  496. /package/{src → dist}/app/plugins/ocr/templates/business_reg.json +0 -0
  497. /package/{src → dist}/app/plugins/ocr/templates/career_cert.json +0 -0
  498. /package/{src → dist}/app/plugins/ocr/templates/driver_license.json +0 -0
  499. /package/{src → dist}/app/plugins/ocr/templates/facility_card.json +0 -0
  500. /package/{src → dist}/app/plugins/ocr/templates/id_card.json +0 -0
  501. /package/{src → dist}/app/plugins/ocr/templates/invoice.json +0 -0
  502. /package/{src → dist}/app/plugins/ocr/templates/namecard.json +0 -0
  503. /package/{src → dist}/app/plugins/ocr/templates/prompts/business_reg.json +0 -0
  504. /package/{src → dist}/app/plugins/ocr/templates/prompts/career_cert.json +0 -0
  505. /package/{src → dist}/app/plugins/ocr/templates/prompts/driver_license.json +0 -0
  506. /package/{src → dist}/app/plugins/ocr/templates/prompts/facility_card.json +0 -0
  507. /package/{src → dist}/app/plugins/ocr/templates/prompts/general.json +0 -0
  508. /package/{src → dist}/app/plugins/ocr/templates/prompts/id_card.json +0 -0
  509. /package/{src → dist}/app/plugins/ocr/templates/prompts/invoice.json +0 -0
  510. /package/{src → dist}/app/plugins/ocr/templates/prompts/namecard.json +0 -0
  511. /package/{src → dist}/app/plugins/ocr/templates/prompts/receipt.json +0 -0
  512. /package/{src → dist}/app/plugins/ocr/templates/receipt.json +0 -0
  513. /package/{src/app/plugins/pg/config.example.json → dist/app/plugins/pg/config.json} +0 -0
  514. /package/{src → dist}/app/plugins/pg/config.ts +0 -0
  515. /package/{src → dist}/app/plugins/pg/docs/README.md +0 -0
  516. /package/{src → dist}/app/plugins/pg/entities/pg_cancel.json +0 -0
  517. /package/{src → dist}/app/plugins/pg/entities/pg_order.json +0 -0
  518. /package/{src → dist}/app/plugins/pg/entities/pg_webhook_log.json +0 -0
  519. /package/{src → dist}/app/plugins/pg/entity-adapter.ts +0 -0
  520. /package/{src → dist}/app/plugins/pg/handlers.ts +0 -0
  521. /package/{src → dist}/app/plugins/pg/index.ts +0 -0
  522. /package/{src → dist}/app/plugins/pg/providers/danal.ts +0 -0
  523. /package/{src → dist}/app/plugins/pg/providers/hecto.ts +0 -0
  524. /package/{src → dist}/app/plugins/pg/providers/index.ts +0 -0
  525. /package/{src → dist}/app/plugins/pg/providers/inicis.ts +0 -0
  526. /package/{src → dist}/app/plugins/pg/providers/kakaopay.ts +0 -0
  527. /package/{src → dist}/app/plugins/pg/providers/kcp.ts +0 -0
  528. /package/{src → dist}/app/plugins/pg/providers/naverpay.ts +0 -0
  529. /package/{src → dist}/app/plugins/pg/providers/payco.ts +0 -0
  530. /package/{src → dist}/app/plugins/pg/providers/payletter.ts +0 -0
  531. /package/{src → dist}/app/plugins/pg/providers/paypal.ts +0 -0
  532. /package/{src → dist}/app/plugins/pg/providers/toss.ts +0 -0
  533. /package/{src → dist}/app/plugins/pg/providers/wanna.ts +0 -0
  534. /package/{src → dist}/app/plugins/pg/routes.ts +0 -0
  535. /package/{src → dist}/app/plugins/pg/service.ts +0 -0
  536. /package/{src → dist}/app/plugins/pg/types/client.ts +0 -0
  537. /package/{src → dist}/app/plugins/pg/types/config.ts +0 -0
  538. /package/{src → dist}/app/plugins/pg/types/error.ts +0 -0
  539. /package/{src → dist}/app/plugins/pg/types/index.ts +0 -0
  540. /package/{src → dist}/app/plugins/pg/types/payment.ts +0 -0
  541. /package/{src → dist}/app/plugins/providers/docs/README.md +0 -0
  542. /package/{src → dist}/app/plugins/providers/solapi-auth.ts +0 -0
  543. /package/{src/app/plugins/push/config.example.json → dist/app/plugins/push/config.json} +0 -0
  544. /package/{src → dist}/app/plugins/push/config.ts +0 -0
  545. /package/{src → dist}/app/plugins/push/docs/README.md +0 -0
  546. /package/{src → dist}/app/plugins/push/entities/push_log.json +0 -0
  547. /package/{src → dist}/app/plugins/push/entities/push_msg.json +0 -0
  548. /package/{src → dist}/app/plugins/push/entity-adapter.ts +0 -0
  549. /package/{src → dist}/app/plugins/push/handlers.ts +0 -0
  550. /package/{src → dist}/app/plugins/push/index.ts +0 -0
  551. /package/{src → dist}/app/plugins/push/providers/apns.ts +0 -0
  552. /package/{src → dist}/app/plugins/push/providers/fcm.ts +0 -0
  553. /package/{src → dist}/app/plugins/push/providers/index.ts +0 -0
  554. /package/{src → dist}/app/plugins/push/providers/utils.ts +0 -0
  555. /package/{src → dist}/app/plugins/push/routes.ts +0 -0
  556. /package/{src → dist}/app/plugins/push/service.ts +0 -0
  557. /package/{src → dist}/app/plugins/push/types/config.ts +0 -0
  558. /package/{src → dist}/app/plugins/push/types/index.ts +0 -0
  559. /package/{src → dist}/app/plugins/push/types/job.ts +0 -0
  560. /package/{src → dist}/app/plugins/shared/docs/README.md +0 -0
  561. /package/{src/app/plugins/sms/config.example.json → dist/app/plugins/sms/config.json} +0 -0
  562. /package/{src → dist}/app/plugins/sms/docs/README.md +0 -0
  563. /package/{src → dist}/app/plugins/sms/entities/sms_log.json +0 -0
  564. /package/{src → dist}/app/plugins/sms/entities/sms_msg.json +0 -0
  565. /package/{src → dist}/app/plugins/sms/entities/sms_verification.json +0 -0
  566. /package/{src → dist}/app/plugins/smtp/config.json +0 -0
  567. /package/{src → dist}/app/plugins/smtp/docs/README.md +0 -0
  568. /package/{src → dist}/app/plugins/smtp/templates/layout.html +0 -0
  569. /package/{src/app/plugins/taxinvoice/config.example.json → dist/app/plugins/taxinvoice/config.json} +0 -0
  570. /package/{src → dist}/app/plugins/taxinvoice/config.ts +0 -0
  571. /package/{src → dist}/app/plugins/taxinvoice/docs/README.md +0 -0
  572. /package/{src → dist}/app/plugins/taxinvoice/entities/tax_invoice.json +0 -0
  573. /package/{src → dist}/app/plugins/taxinvoice/entities/tax_invoice_item.json +0 -0
  574. /package/{src → dist}/app/plugins/taxinvoice/entities/tax_invoice_log.json +0 -0
  575. /package/{src → dist}/app/plugins/taxinvoice/entities/tax_invoice_party.json +0 -0
  576. /package/{src → dist}/app/plugins/taxinvoice/entity-adapter.ts +0 -0
  577. /package/{src → dist}/app/plugins/taxinvoice/handlers.ts +0 -0
  578. /package/{src → dist}/app/plugins/taxinvoice/index.ts +0 -0
  579. /package/{src → dist}/app/plugins/taxinvoice/providers/barobill.ts +0 -0
  580. /package/{src → dist}/app/plugins/taxinvoice/providers/bolta.ts +0 -0
  581. /package/{src → dist}/app/plugins/taxinvoice/providers/esero.ts +0 -0
  582. /package/{src → dist}/app/plugins/taxinvoice/providers/index.ts +0 -0
  583. /package/{src → dist}/app/plugins/taxinvoice/providers/popbill.ts +0 -0
  584. /package/{src → dist}/app/plugins/taxinvoice/providers/sendbill.ts +0 -0
  585. /package/{src → dist}/app/plugins/taxinvoice/providers/smartbill.ts +0 -0
  586. /package/{src → dist}/app/plugins/taxinvoice/routes.ts +0 -0
  587. /package/{src → dist}/app/plugins/taxinvoice/service.ts +0 -0
  588. /package/{src → dist}/app/plugins/taxinvoice/types/client.ts +0 -0
  589. /package/{src → dist}/app/plugins/taxinvoice/types/config.ts +0 -0
  590. /package/{src → dist}/app/plugins/taxinvoice/types/index.ts +0 -0
  591. /package/{src → dist}/app/plugins/taxinvoice/types/invoice.ts +0 -0
  592. /package/{src → dist}/app/plugins/taxinvoice/types/queue.ts +0 -0
  593. /package/{src → dist}/app/plugins/vessel_kr/config.json +0 -0
  594. /package/{src → dist}/app/plugins/vessel_kr/config.ts +0 -0
  595. /package/{src → dist}/app/plugins/vessel_kr/docs/README.md +0 -0
  596. /package/{src → dist}/app/plugins/vessel_kr/entities/vessel_kr_entry.json +0 -0
  597. /package/{src → dist}/app/plugins/vessel_kr/handlers.ts +0 -0
  598. /package/{src → dist}/app/plugins/vessel_kr/index.ts +0 -0
  599. /package/{src → dist}/app/plugins/vessel_kr/routes.ts +0 -0
  600. /package/{src → dist}/app/plugins/vessel_kr/service.ts +0 -0
  601. /package/{src → dist}/app/plugins/vessel_kr/types/index.ts +0 -0
  602. /package/{src → dist}/app/routes/README.md +0 -0
  603. /package/{src → dist}/app/routes/account/change-password/config.json +0 -0
  604. /package/{src → dist}/app/routes/account/change-password/entities/password_history.json +0 -0
  605. /package/{src → dist}/app/routes/account/change-password/handlers.ts +0 -0
  606. /package/{src → dist}/app/routes/account/change-password/routes.ts +0 -0
  607. /package/{src → dist}/app/routes/account/config.json +0 -0
  608. /package/{src → dist}/app/routes/account/reactivate/config.json +0 -0
  609. /package/{src → dist}/app/routes/account/reactivate/handlers.ts +0 -0
  610. /package/{src → dist}/app/routes/account/reactivate/routes.ts +0 -0
  611. /package/{src → dist}/app/routes/account/register/config-loader.ts +0 -0
  612. /package/{src → dist}/app/routes/account/register/config.json +0 -0
  613. /package/{src → dist}/app/routes/account/register/handlers.ts +0 -0
  614. /package/{src → dist}/app/routes/account/register/routes.ts +0 -0
  615. /package/{src → dist}/app/routes/account/register/types/index.ts +0 -0
  616. /package/{src → dist}/app/routes/account/routes.ts +0 -0
  617. /package/{src → dist}/app/routes/account/templates/force_reset.html +0 -0
  618. /package/{src → dist}/app/routes/account/templates/welcome.html +0 -0
  619. /package/{src → dist}/app/routes/account/withdraw/handlers.ts +0 -0
  620. /package/{src → dist}/app/routes/account/withdraw/routes.ts +0 -0
  621. /package/{src → dist}/app/routes/approval/config.json +0 -0
  622. /package/{src → dist}/app/routes/approval/entities/approval.json +0 -0
  623. /package/{src → dist}/app/routes/approval/entities/comments.json +0 -0
  624. /package/{src → dist}/app/routes/approval/entities/reference.json +0 -0
  625. /package/{src → dist}/app/routes/approval/routes.ts +0 -0
  626. /package/{src → dist}/app/routes/auth/config.json +0 -0
  627. /package/{src → dist}/app/routes/auth/handlers.ts +0 -0
  628. /package/{src → dist}/app/routes/auth/routes.ts +0 -0
  629. /package/{src → dist}/app/routes/board/config.json +0 -0
  630. /package/{src → dist}/app/routes/board/entities/board_category.json +0 -0
  631. /package/{src → dist}/app/routes/board/entities/board_comment.json +0 -0
  632. /package/{src → dist}/app/routes/board/entities/board_like.json +0 -0
  633. /package/{src → dist}/app/routes/board/entities/board_mention.json +0 -0
  634. /package/{src → dist}/app/routes/board/entities/board_post.json +0 -0
  635. /package/{src → dist}/app/routes/board/entities/board_post_tag.json +0 -0
  636. /package/{src → dist}/app/routes/board/entities/board_rating.json +0 -0
  637. /package/{src → dist}/app/routes/board/entities/board_read_log.json +0 -0
  638. /package/{src → dist}/app/routes/board/entities/board_report.json +0 -0
  639. /package/{src → dist}/app/routes/board/entities/board_tag.json +0 -0
  640. /package/{src → dist}/app/routes/board/handlers/categories.ts +0 -0
  641. /package/{src → dist}/app/routes/board/handlers/comments.ts +0 -0
  642. /package/{src → dist}/app/routes/board/handlers/files.ts +0 -0
  643. /package/{src → dist}/app/routes/board/handlers/likes.ts +0 -0
  644. /package/{src → dist}/app/routes/board/handlers/mentions.ts +0 -0
  645. /package/{src → dist}/app/routes/board/handlers/posts.ts +0 -0
  646. /package/{src → dist}/app/routes/board/handlers/ratings.ts +0 -0
  647. /package/{src → dist}/app/routes/board/handlers/reports.ts +0 -0
  648. /package/{src → dist}/app/routes/board/handlers/tags.ts +0 -0
  649. /package/{src → dist}/app/routes/board/routes.ts +0 -0
  650. /package/{src → dist}/app/routes/password-reset/config.example.json +0 -0
  651. /package/{src → dist}/app/routes/password-reset/config.json +0 -0
  652. /package/{src → dist}/app/routes/password-reset/entities/account.json +0 -0
  653. /package/{src → dist}/app/routes/password-reset/handlers.ts +0 -0
  654. /package/{src → dist}/app/routes/password-reset/password-utils.ts +0 -0
  655. /package/{src → dist}/app/routes/password-reset/routes.ts +0 -0
  656. /package/{src → dist}/app/routes/password-reset/templates/password_reset.html +0 -0
  657. /package/{src → dist}/app/routes/password-reset/templates/password_reset_link.html +0 -0
  658. /package/{src → dist}/app/routes/password-reset/types/index.ts +0 -0
  659. /package/{src → dist}/app/routes/privilege/config.json +0 -0
  660. /package/{src → dist}/app/routes/privilege/entities/pv_group.json +0 -0
  661. /package/{src → dist}/app/routes/privilege/entities/pv_group_item.json +0 -0
  662. /package/{src → dist}/app/routes/privilege/entities/pv_item.json +0 -0
  663. /package/{src → dist}/app/routes/privilege/entities/user_pv_group.json +0 -0
  664. /package/{src → dist}/app/routes/privilege/entities/user_pv_item.json +0 -0
  665. /package/{src → dist}/app/routes/privilege/routes.ts +0 -0
  666. /package/{src → dist}/app/routes/user/config.json +0 -0
  667. /package/{src → dist}/app/routes/user/entities/user.json +0 -0
  668. /package/{src → dist}/app/routes/user/entities/user_biometric.json +0 -0
  669. /package/{src → dist}/app/routes/user/routes.ts +0 -0
  670. /package/{src → dist}/app/schedules/README.md +0 -0
  671. /package/{src → dist}/app/schedules/data-retention/config.json +0 -0
  672. /package/{src → dist}/app/schedules/data-retention/index.ts +0 -0
  673. /package/{src → dist}/app/schedules/dormancy/config.json +0 -0
  674. /package/{src → dist}/app/schedules/dormancy/entities/account.json +0 -0
  675. /package/{src → dist}/app/schedules/dormancy/entities/privacy_cron_lock.json +0 -0
  676. /package/{src → dist}/app/schedules/dormancy/index.ts +0 -0
  677. /package/{src → dist}/app/schedules/dormancy/templates/dormancy_completed.html +0 -0
  678. /package/{src → dist}/app/schedules/dormancy/templates/dormancy_warning.html +0 -0
  679. /package/{configs → dist/configs}/cache.json +0 -0
  680. /package/{configs → dist/configs}/cors.json +0 -0
  681. /package/{configs → dist/configs}/database.json +0 -0
  682. /package/{configs → dist/configs}/security.json +0 -0
  683. /package/{configs → dist/configs}/server.json +0 -0
  684. /package/{docs → dist/docs}/README.md +0 -0
  685. /package/{docs → dist/docs}/architecture.md +0 -0
  686. /package/{docs → dist/docs}/cache.md +0 -0
  687. /package/{docs → dist/docs}/configs.md +0 -0
  688. /package/{docs → dist/docs}/database.md +0 -0
  689. /package/{docs → dist/docs}/flows.md +0 -0
  690. /package/{docs → dist/docs}/getting-started.md +0 -0
  691. /package/{docs → dist/docs}/hooks.md +0 -0
  692. /package/{docs → dist/docs}/internals.md +0 -0
  693. /package/{docs → dist/docs}/plugins/2fa.md +0 -0
  694. /package/{docs → dist/docs}/plugins/alimtalk.md +0 -0
  695. /package/{docs → dist/docs}/plugins/friendtalk.md +0 -0
  696. /package/{docs → dist/docs}/plugins/holidays.md +0 -0
  697. /package/{docs → dist/docs}/plugins/how-to-create.md +0 -0
  698. /package/{docs → dist/docs}/plugins/identity.md +0 -0
  699. /package/{docs → dist/docs}/plugins/llm.md +0 -0
  700. /package/{docs → dist/docs}/plugins/oauth.md +0 -0
  701. /package/{docs → dist/docs}/plugins/ocr.md +0 -0
  702. /package/{docs → dist/docs}/plugins/pg.md +0 -0
  703. /package/{docs → dist/docs}/plugins/push.md +0 -0
  704. /package/{docs → dist/docs}/plugins/sms.md +0 -0
  705. /package/{docs → dist/docs}/plugins/taxinvoice.md +0 -0
  706. /package/{docs → dist/docs}/routes/README.md +0 -0
  707. /package/{docs → dist/docs}/routes/account-routes.md +0 -0
  708. /package/{docs → dist/docs}/routes/alimtalk-routes.md +0 -0
  709. /package/{docs → dist/docs}/routes/board-routes.md +0 -0
  710. /package/{docs → dist/docs}/routes/email-verification.md +0 -0
  711. /package/{docs → dist/docs}/routes/friendtalk-routes.md +0 -0
  712. /package/{docs → dist/docs}/routes/holidays-routes.md +0 -0
  713. /package/{docs → dist/docs}/routes/how-to-create.md +0 -0
  714. /package/{docs → dist/docs}/routes/identity-routes.md +0 -0
  715. /package/{docs → dist/docs}/routes/llm-routes.md +0 -0
  716. /package/{docs → dist/docs}/routes/ocr-routes.md +0 -0
  717. /package/{docs → dist/docs}/routes/password-reset.md +0 -0
  718. /package/{docs → dist/docs}/routes/pg-routes.md +0 -0
  719. /package/{docs → dist/docs}/routes/push-routes.md +0 -0
  720. /package/{docs → dist/docs}/routes/sms-routes.md +0 -0
  721. /package/{docs → dist/docs}/routes/smtp-routes.md +0 -0
  722. /package/{docs → dist/docs}/routes/tax-invoice-routes.md +0 -0
  723. /package/{docs → dist/docs}/schedules/dormancy-and-retention.md +0 -0
  724. /package/{docs → dist/docs}/schedules/how-to-create.md +0 -0
  725. /package/{docs → dist/docs}/scripts-guide.md +0 -0
  726. /package/{docs → dist/docs}/security.md +0 -0
  727. /package/{docs → dist/docs}/system.md +0 -0
  728. /package/{scripts → dist/scripts}/entity.sh +0 -0
  729. /package/{scripts → dist/scripts}/reset-all.sh +0 -0
  730. /package/{scripts → dist/scripts}/run.sh +0 -0
  731. /package/{scripts/dist-tsconfig.json → dist/tsconfig.json} +0 -0
@@ -1,2342 +0,0 @@
1
- # 범용 게시판 API 설계
2
-
3
- > **작성일**: 2026-03-06
4
- > **상태**: 🔍 검토 중
5
- > **관련**: `gateway-server-architecture.md`, `plugin-models.md`
6
-
7
- ---
8
-
9
- ## 목차
10
-
11
- 1. [개요](#1-개요)
12
- 2. [용어 정의](#2-용어-정의)
13
- 3. [아키텍처 흐름](#3-아키텍처-흐름)
14
- 4. [엔티티 설계](#4-엔티티-설계)
15
- 5. [앱서버 Routes 설계](#5-앱서버-routes-설계)
16
- 6. [API 상세 스펙](#6-api-상세-스펙)
17
- 7. [권한 및 보안](#7-권한-및-보안)
18
- 8. [확장 고려사항](#8-확장-고려사항)
19
-
20
- ---
21
-
22
- ## 1. 개요
23
-
24
- 여러 프로젝트(codeshop, elim_crm, elim_gas 등)에서 공통으로 사용할 수 있는 **범용 게시판 API**를 설계한다.
25
- 각 프로젝트는 **entity-app-server**(Fastify/TypeScript)를 사용하며,
26
- 게시판은 앱서버의 `routes/board/` 에 구현한다.
27
-
28
- ### 핵심 요구사항
29
-
30
- - 게시판 종류를 동적으로 생성/관리 (카테고리)
31
- - 게시글 CRUD + 목록 조회 (페이징, 검색, 정렬)
32
- - **답글**: 게시글에 대한 계층형 답변. `board_post` 엔티티에서 `parent_seq`로 자기참조 트리 구성 (depth 무제한)
33
- - **댓글**: 게시글·답글 내부의 짧은 코멘트 (단일 depth)
34
- - 파일 첨부
35
- - 조회수 카운트
36
- - 고정글(pin) 지원
37
- - 권한 제어 (읽기/쓰기/관리)
38
-
39
- ---
40
-
41
- ## 2. 용어 정의
42
-
43
- | 용어 | 엔티티 | 설명 |
44
- | ------------ | ----------------------------------- | ------------------------------------------------------------------------------------------- |
45
- | **게시글** | `board_post` | 게시판의 원글. `parent_seq=null`, `depth=0`. |
46
- | **답글** | `board_post` | 게시글에 대한 계층형 답변. **동일 엔티티** `board_post`에서 `parent_seq`로 트리를 형성한다. |
47
- | **댓글** | `board_comment` | 게시글 또는 답글에 달리는 짧은 코멘트. 단일 depth. |
48
- | **카테고리** | `board_category` | 게시판 종류 (공지사항, 자유게시판, Q&A 등) |
49
- | **첨부파일** | `file_meta` (엔티티 서버 files API) | 게시글에 첨부된 파일. 엔티티 서버의 파일 스토리지를 사용한다. |
50
-
51
- > **핵심**: 원글과 답글은 모두 `board_post` 엔티티 하나에 저장된다. 목록은 하나의 API로 원글·답글을 함께 조회하며,
52
- > `parent_seq` / `depth` / `root_seq` 필드로 계층 구조를 표현한다.
53
-
54
- ```
55
- board_post (seq:42, parent_seq=null, depth=0) ← 원글
56
- ├── board_post (seq:10, parent_seq=42, root_seq=42, depth=1) ← 답글
57
- │ ├── board_post (seq:15, parent_seq=10, root_seq=42, depth=2) ← 대답변
58
- │ │ └── 댓글 (board_comment, post_seq=15) ← 대답변 코멘트
59
- │ └── 댓글 (board_comment, post_seq=10) ← 답글 코멘트
60
- ├── board_post (seq:11, parent_seq=42, root_seq=42, depth=1) ← 답글 B
61
- ├── 댓글 1 (board_comment, post_seq=42) ← 원글 코멘트
62
- ├── 댓글 2 (board_comment, post_seq=42)
63
- └── 첨부파일 (file_meta) ← /v1/files/board_post/upload?entity_seq=42
64
- ```
65
-
66
- ---
67
-
68
- ## 3. 아키텍처 흐름
69
-
70
- ```
71
- ┌──────────────┐ ┌───────────────────────┐ ┌──────────────────┐
72
- │ Frontend │────▶│ App Server │────▶│ Entity Server │
73
- │ (Vue/React) │◀────│ (Fastify/TypeScript) │◀────│ (Go/Fiber) │
74
- └──────────────┘ └───────────────────────┘ └──────────────────┘
75
- │ │
76
- │ entity-app-server │ Entities:
77
- │ routes/board/route.ts │ board_category
78
- │ │ board_post
79
- │ 역할: │ board_comment
80
- │ - JWT 인증 (auth plugin) │ (parent_seq 자기참조)
81
- │ - 입력 검증 │
82
- │ - 권한 확인 │ Files API:
83
- │ - 파일: files API 프록시 │ /v1/files/board_post/*
84
- │ - entityServer.* SDK 호출 │ 역할:
85
- │ - 응답 가공 │ - CRUD 처리
86
- │ │ - 데이터 검증
87
- │ @gateway/api 사용: │ - Hook 실행
88
- │ ok(), fail(), entityServer │ - 이력 관리
89
- └──────────────────────────────┘
90
- ```
91
-
92
- ### 요청 흐름 예시: 게시글 작성
93
-
94
- ```
95
- Frontend App Server (Fastify) Entity Server (Go)
96
- │ │ │
97
- │ POST /api/v1/board/{category}/submit │ │
98
- │ { title, content, parent_seq, ... } │ │
99
- │───────────────────────────▶│ │
100
- │ │ 1. preHandler: authenticate │
101
- │ │ 2. 입력 검증 │
102
- │ │ 3. 카테고리 권한 확인 │
103
- │ │ │
104
- │ │ entityServer.submit("board_post",│
105
- │ │ { category_seq, title, ... }) │
106
- │ │──────────────────────────────────▶│
107
- │ │ │ validate
108
- │ │ │ insert
109
- │ │ │ run hooks
110
- │ │ { seq, ... } │
111
- │ │◀──────────────────────────────────│
112
- │ ok({ seq }) │ │
113
- │◀───────────────────────────│ │
114
- ```
115
-
116
- ---
117
-
118
- ## 4. 엔티티 설계
119
-
120
- ### 4.1 board_category (게시판 카테고리)
121
-
122
- 게시판 종류를 정의한다. 공지사항, 자유게시판, Q&A 등.
123
-
124
- ```json
125
- {
126
- "name": "board_category",
127
- "description": "게시판 카테고리 (게시판 종류 정의)",
128
- "fields": {
129
- "name": {
130
- "index": true,
131
- "required": true,
132
- "unique": true,
133
- "comment": "게시판 이름 (예: notice, free, qna)"
134
- },
135
- "label": {
136
- "required": true,
137
- "comment": "표시 이름 (예: 공지사항, 자유게시판)"
138
- },
139
- "description": {
140
- "comment": "게시판 설명"
141
- },
142
- "status": {
143
- "index": true,
144
- "type": ["active", "inactive"],
145
- "default": "active",
146
- "comment": "게시판 활성 상태"
147
- },
148
- "sort_order": {
149
- "type": "uint",
150
- "default": "0",
151
- "comment": "정렬 순서"
152
- },
153
- "read_role": {
154
- "type": ["all", "member", "admin"],
155
- "default": "all",
156
- "comment": "읽기 권한"
157
- },
158
- "write_role": {
159
- "type": ["all", "member", "admin"],
160
- "default": "member",
161
- "comment": "쓰기 권한"
162
- },
163
- "reply_enabled": {
164
- "type": ["Y", "N"],
165
- "default": "Y",
166
- "comment": "답글 허용 여부"
167
- },
168
- "comment_enabled": {
169
- "type": ["Y", "N"],
170
- "default": "Y",
171
- "comment": "댓글 허용 여부"
172
- },
173
- "file_enabled": {
174
- "type": ["Y", "N"],
175
- "default": "Y",
176
- "comment": "파일 첨부 허용 여부"
177
- },
178
- "guest_write_enabled": {
179
- "type": ["Y", "N"],
180
- "default": "N",
181
- "comment": "비회원 글작성 허용 여부 (비밀번호 인증)"
182
- },
183
- "like_enabled": {
184
- "type": ["Y", "N"],
185
- "default": "N",
186
- "comment": "좋아요/추천 허용 여부"
187
- },
188
- "rating_enabled": {
189
- "type": ["Y", "N"],
190
- "default": "N",
191
- "comment": "별점 허용 여부"
192
- },
193
- "accept_enabled": {
194
- "type": ["Y", "N"],
195
- "default": "N",
196
- "comment": "답변 채택 허용 여부 (Q&A 게시판용)"
197
- },
198
- "push_on_write": {
199
- "type": ["Y", "N"],
200
- "default": "N",
201
- "comment": "글 등록 시 관리자 push 알림 여부 (문의게시판 등)"
202
- },
203
- "view_count_mode": {
204
- "type": ["always", "session", "daily", "once"],
205
- "default": "daily",
206
- "comment": "조회수 카운트 방식. always=매 조회, session=세션당 1회, daily=1일 1인 1회(기본), once=1인 1회(영구)"
207
- },
208
- "anonymous_enabled": {
209
- "type": ["Y", "N"],
210
- "default": "N",
211
- "comment": "익명 게시판 여부. Y이면 작성자 표시를 '익명'으로 강제, user_seq는 응답에서 숨김"
212
- }
213
- }
214
- }
215
- ```
216
-
217
- ### 4.2 board_post (게시글 · 답글)
218
-
219
- 원글과 답글을 **동일 엔티티**에서 관리한다. `parent_seq=null`이면 원글(depth=0),
220
- 값이 있으면 답글이다. 목록 API는 `category_seq` 기준으로 전체(원글+답글)를 한 번에 반환하며,
221
- 프론트엔드에서 `parent_seq`와 `depth`를 이용해 트리 구조로 조립한다.
222
-
223
- ```json
224
- {
225
- "name": "board_post",
226
- "description": "게시판 원글 및 답글 (자기참조 트리)",
227
- "fields": {
228
- "category_seq": {
229
- "index": true,
230
- "required": true,
231
- "type": "uint",
232
- "comment": "게시판 카테고리 seq"
233
- },
234
- "user_seq": {
235
- "index": true,
236
- "type": "uint",
237
- "comment": "작성자 user seq (비회원이면 null)"
238
- },
239
- "author_name": {
240
- "required": true,
241
- "comment": "작성자 표시 이름 (회원: 닉네임, 비회원: 입력값)"
242
- },
243
- "guest_password": {
244
- "comment": "비회원 비밀번호 (bcrypt 해시, 비회원 글만 사용)"
245
- },
246
- "title": {
247
- "index": true,
248
- "required": true,
249
- "comment": "제목"
250
- },
251
- "content": {
252
- "required": true,
253
- "comment": "본문 (HTML or Markdown)"
254
- },
255
- "status": {
256
- "index": true,
257
- "type": ["published", "draft", "deleted"],
258
- "default": "published",
259
- "comment": "글 상태"
260
- },
261
- "pinned": {
262
- "index": true,
263
- "type": ["Y", "N"],
264
- "default": "N",
265
- "comment": "상단 고정 여부"
266
- },
267
- "view_count": {
268
- "type": "uint",
269
- "default": "0",
270
- "comment": "조회수"
271
- },
272
- "reply_count": {
273
- "type": "uint",
274
- "default": "0",
275
- "comment": "답글 수 (캐시)"
276
- },
277
- "comment_count": {
278
- "type": "uint",
279
- "default": "0",
280
- "comment": "댓글 수 (캐시)"
281
- },
282
- "file_count": {
283
- "type": "uint",
284
- "default": "0",
285
- "comment": "첨부파일 수 (캐시, 앱서버에서 업로드/삭제 시 갱신)"
286
- },
287
- "parent_seq": {
288
- "index": true,
289
- "type": "uint",
290
- "comment": "부모 글 seq (null=원글, 값 있으면 답글)"
291
- },
292
- "root_seq": {
293
- "index": true,
294
- "type": "uint",
295
- "comment": "최상위 원글 seq (답글 조회 필터링용, 원글은 자기 자신 seq)"
296
- },
297
- "depth": {
298
- "type": "uint",
299
- "default": "0",
300
- "comment": "깊이 (0=원글, 1=직접 답변, 2=대답변, ...)"
301
- },
302
- "like_count": {
303
- "type": "uint",
304
- "default": "0",
305
- "comment": "좋아요 수 (캐시)"
306
- },
307
- "rating_sum": {
308
- "type": "uint",
309
- "default": "0",
310
- "comment": "별점 합계 (캐시, rating_avg = rating_sum / rating_count)"
311
- },
312
- "rating_count": {
313
- "type": "uint",
314
- "default": "0",
315
- "comment": "별점 참여 수 (캐시)"
316
- },
317
- "accepted": {
318
- "index": true,
319
- "type": ["Y", "N"],
320
- "default": "N",
321
- "comment": "채택된 답변 여부 (accept_enabled 게시판의 답글에만 사용)"
322
- }
323
- },
324
- "fk": {
325
- "category_seq": "board_category.seq",
326
- "user_seq": "user.seq",
327
- "parent_seq": "board_post.seq",
328
- "root_seq": "board_post.seq"
329
- },
330
- "hooks": {
331
- "after_insert": [
332
- {
333
- "description": "답글 작성 시 원글의 reply_count 증가 (원글 = root_seq 기준. depth에 관계없이 모든 하위 답글을 원글 1건으로 카운트)",
334
- "type": "update",
335
- "entity": "board_post",
336
- "condition": "${new.parent_seq} != null",
337
- "match": { "seq": "${new.root_seq}" },
338
- "data": { "reply_count": "${target.reply_count + 1}" }
339
- },
340
- {
341
- "description": "글 등록 시 카테고리 push_on_write=Y이면 관리자에게 push 알림",
342
- "type": "push",
343
- "condition": "${category.push_on_write} == 'Y'",
344
- "target": "role:admin",
345
- "title": "[${category.label}] 새 글이 등록되었습니다",
346
- "body": "${new.author_name}: ${new.title}",
347
- "data": {
348
- "type": "board_post",
349
- "seq": "${new.seq}",
350
- "category": "${category.name}"
351
- }
352
- }
353
- ],
354
- "after_delete": [
355
- {
356
- "description": "답글 삭제 시 원글의 reply_count 감소 (root_seq 기준)",
357
- "type": "update",
358
- "entity": "board_post",
359
- "condition": "${old.parent_seq} != null",
360
- "match": { "seq": "${old.root_seq}" },
361
- "data": { "reply_count": "${target.reply_count - 1}" }
362
- }
363
- ]
364
- }
365
- }
366
- ```
367
-
368
- ### 4.3 board_comment (댓글)
369
-
370
- 게시글 또는 답글에 달리는 짧은 코멘트. 원글·답글 모두 `board_post` 엔티티이므로
371
- `post_seq` 하나로 대상을 지정한다. `root_seq`는 원글의 전체 댓글을 한 번에 조회할 때 사용한다.
372
-
373
- ```json
374
- {
375
- "name": "board_comment",
376
- "description": "게시글/답글 댓글 (단일 depth)",
377
- "fields": {
378
- "post_seq": {
379
- "index": true,
380
- "required": true,
381
- "type": "uint",
382
- "comment": "댓글 대상 board_post seq (원글 또는 답글)"
383
- },
384
- "root_seq": {
385
- "index": true,
386
- "required": true,
387
- "type": "uint",
388
- "comment": "최상위 원글 seq (해당 원글의 전체 댓글 조회용)"
389
- },
390
- "user_seq": {
391
- "index": true,
392
- "required": true,
393
- "type": "uint",
394
- "comment": "작성자 user seq"
395
- },
396
- "author_name": {
397
- "required": true,
398
- "comment": "작성자 표시 이름"
399
- },
400
- "content": {
401
- "required": true,
402
- "comment": "댓글 내용"
403
- },
404
- "status": {
405
- "index": true,
406
- "type": ["active", "deleted"],
407
- "default": "active",
408
- "comment": "댓글 상태"
409
- },
410
- "rating_sum": {
411
- "type": "uint",
412
- "default": "0",
413
- "comment": "별점 합계 (캐시, rating_avg = rating_sum / rating_count)"
414
- },
415
- "rating_count": {
416
- "type": "uint",
417
- "default": "0",
418
- "comment": "별점 참여 수 (캐시)"
419
- }
420
- },
421
- "fk": {
422
- "post_seq": "board_post.seq",
423
- "root_seq": "board_post.seq",
424
- "user_seq": "user.seq"
425
- },
426
- "hooks": {
427
- "after_insert": [
428
- {
429
- "description": "댓글 작성 시 대상 board_post.comment_count 증가",
430
- "type": "update",
431
- "entity": "board_post",
432
- "match": { "seq": "${new.post_seq}" },
433
- "data": { "comment_count": "${target.comment_count + 1}" }
434
- }
435
- ],
436
- "after_delete": [
437
- {
438
- "description": "댓글 삭제 시 대상 board_post.comment_count 감소",
439
- "type": "update",
440
- "entity": "board_post",
441
- "match": { "seq": "${old.post_seq}" },
442
- "data": { "comment_count": "${target.comment_count - 1}" }
443
- }
444
- ]
445
- }
446
- }
447
- ```
448
-
449
- ### 4.4 첨부파일 — 엔티티 서버 Files API
450
-
451
- `board_file` 별도 엔티티를 만들지 않는다. 엔티티 서버의 내장 파일 스토리지(`/v1/files/:entity/*`)를 사용하며,
452
- `board_post` 엔티티를 스코프로 하여 `entity_seq`(게시글 seq)로 파일을 연결한다.
453
-
454
- **엔티티 서버 Files API 라우트:**
455
-
456
- | 동작 | 엔드포인트 | 설명 |
457
- | -------- | ------------------------------------------- | ----------------------------------------------- |
458
- | 업로드 | `POST /v1/files/board_post/upload` | `entity_seq={postSeq}`, `field_name=attachment` |
459
- | 목록 | `POST /v1/files/board_post/list` | `ref_seq={postSeq}` |
460
- | 다운로드 | `GET /v1/files/{uuid}` | 브라우저 인라인 뷰 (공개 파일) |
461
- | 다운로드 | `POST /v1/files/board_post/download/{uuid}` | API 다운로드 (항상 attachment) |
462
- | 삭제 | `POST /v1/files/board_post/delete/{uuid}` | |
463
- | 메타 | `POST /v1/files/board_post/meta/{uuid}` | |
464
-
465
- **앱서버 SDK 사용:**
466
-
467
- ```typescript
468
- // 업로드
469
- const res = await entityServer.fileUpload("board_post", file, {
470
- refSeq: postSeq,
471
- isPublic: false,
472
- });
473
- // res.uuid → 파일 식별자
474
- // res.data → FileMeta (uuid, original_name, mime_type, size, account_seq, created_time 등)
475
-
476
- // 목록
477
- const list = await entityServer.fileList("board_post", { refSeq: postSeq });
478
-
479
- // 삭제
480
- await entityServer.fileDelete("board_post", uuid);
481
- ```
482
-
483
- **file_count 갱신**: `board_post.file_count`는 별도 hook이 없으므로, 업로드·삭제 후 앱서버 핸들러에서
484
- `entityServer.submit("board_post", { seq: postSeq, file_count: newCount })` 로 갱신한다.
485
-
486
- ### 4.5 board_like (좋아요 · 추천)
487
-
488
- 게시글 또는 답글에 대한 좋아요. `post_seq + user_seq` 유니크로 중복 방지.
489
- 비회원은 좋아요 불가 (IP 기반 방식 필요 시 `guest_ip` 필드 추가 가능).
490
-
491
- ```json
492
- {
493
- "name": "board_like",
494
- "description": "게시글/답글 좋아요",
495
- "fields": {
496
- "post_seq": {
497
- "index": true,
498
- "required": true,
499
- "type": "uint",
500
- "comment": "대상 board_post seq"
501
- },
502
- "user_seq": {
503
- "index": true,
504
- "required": true,
505
- "type": "uint",
506
- "comment": "좋아요한 user seq"
507
- }
508
- },
509
- "unique": ["post_seq", "user_seq"],
510
- "fk": {
511
- "post_seq": "board_post.seq",
512
- "user_seq": "user.seq"
513
- },
514
- "hooks": {
515
- "after_insert": [
516
- {
517
- "type": "update",
518
- "entity": "board_post",
519
- "match": { "seq": "${new.post_seq}" },
520
- "data": { "like_count": "${target.like_count + 1}" }
521
- }
522
- ],
523
- "after_delete": [
524
- {
525
- "type": "update",
526
- "entity": "board_post",
527
- "match": { "seq": "${old.post_seq}" },
528
- "data": { "like_count": "${target.like_count - 1}" }
529
- }
530
- ]
531
- }
532
- }
533
- ```
534
-
535
- ### 4.6 board_rating (별점)
536
-
537
- 게시글·답글·댓글 모두에 별점을 줄 수 있다. `target_type` + `target_seq` + `user_seq` 유니크로 중복 방지.
538
- hook은 `target_type`에 따라 `board_post` 또는 `board_comment`의 캐시 필드를 갱신한다.
539
- 평균은 `rating_sum / rating_count`로 계산한다.
540
-
541
- ```json
542
- {
543
- "name": "board_rating",
544
- "description": "게시글/답글/댓글 별점 (1~5)",
545
- "fields": {
546
- "target_type": {
547
- "index": true,
548
- "required": true,
549
- "type": ["post", "comment"],
550
- "comment": "별점 대상 유형 (post=게시글/답글, comment=댓글)"
551
- },
552
- "target_seq": {
553
- "index": true,
554
- "required": true,
555
- "type": "uint",
556
- "comment": "대상 엔티티 seq (board_post.seq 또는 board_comment.seq)"
557
- },
558
- "user_seq": {
559
- "index": true,
560
- "required": true,
561
- "type": "uint",
562
- "comment": "평가한 user seq"
563
- },
564
- "score": {
565
- "required": true,
566
- "type": ["1", "2", "3", "4", "5"],
567
- "comment": "별점 (1~5)"
568
- }
569
- },
570
- "unique": ["target_type", "target_seq", "user_seq"],
571
- "fk": {
572
- "user_seq": "user.seq"
573
- },
574
- "hooks": {
575
- "after_insert": [
576
- {
577
- "description": "게시글/답글 별점 캐시 갱신",
578
- "type": "update",
579
- "entity": "board_post",
580
- "condition": "${new.target_type} == 'post'",
581
- "match": { "seq": "${new.target_seq}" },
582
- "data": {
583
- "rating_sum": "${target.rating_sum + new.score}",
584
- "rating_count": "${target.rating_count + 1}"
585
- }
586
- },
587
- {
588
- "description": "댓글 별점 캐시 갱신",
589
- "type": "update",
590
- "entity": "board_comment",
591
- "condition": "${new.target_type} == 'comment'",
592
- "match": { "seq": "${new.target_seq}" },
593
- "data": {
594
- "rating_sum": "${target.rating_sum + new.score}",
595
- "rating_count": "${target.rating_count + 1}"
596
- }
597
- }
598
- ],
599
- "after_update": [
600
- {
601
- "description": "게시글/답글 별점 수정 시 합계 보정",
602
- "type": "update",
603
- "entity": "board_post",
604
- "condition": "${new.target_type} == 'post'",
605
- "match": { "seq": "${new.target_seq}" },
606
- "data": {
607
- "rating_sum": "${target.rating_sum - old.score + new.score}"
608
- }
609
- },
610
- {
611
- "description": "댓글 별점 수정 시 합계 보정",
612
- "type": "update",
613
- "entity": "board_comment",
614
- "condition": "${new.target_type} == 'comment'",
615
- "match": { "seq": "${new.target_seq}" },
616
- "data": {
617
- "rating_sum": "${target.rating_sum - old.score + new.score}"
618
- }
619
- }
620
- ],
621
- "after_delete": [
622
- {
623
- "description": "게시글/답글 별점 취소 캐시 보정",
624
- "type": "update",
625
- "entity": "board_post",
626
- "condition": "${old.target_type} == 'post'",
627
- "match": { "seq": "${old.target_seq}" },
628
- "data": {
629
- "rating_sum": "${target.rating_sum - old.score}",
630
- "rating_count": "${target.rating_count - 1}"
631
- }
632
- },
633
- {
634
- "description": "댓글 별점 취소 캐시 보정",
635
- "type": "update",
636
- "entity": "board_comment",
637
- "condition": "${old.target_type} == 'comment'",
638
- "match": { "seq": "${old.target_seq}" },
639
- "data": {
640
- "rating_sum": "${target.rating_sum - old.score}",
641
- "rating_count": "${target.rating_count - 1}"
642
- }
643
- }
644
- ]
645
- }
646
- }
647
- ```
648
-
649
- ### 4.7 board_tag (태그)
650
-
651
- 게시글에 붙이는 태그. `name`은 소문자 슬러그(`javascript`, `vue-js`), `label`은 표시명.
652
- `use_count`는 `board_post_tag` hook으로 자동 갱신된다.
653
-
654
- ```json
655
- {
656
- "name": "board_tag",
657
- "description": "게시글 태그",
658
- "fields": {
659
- "name": {
660
- "index": true,
661
- "required": true,
662
- "unique": true,
663
- "comment": "태그 슬러그 (예: javascript, vue-js) — lowercase, 하이픈 허용"
664
- },
665
- "label": {
666
- "required": true,
667
- "comment": "표시명 (예: JavaScript, Vue.js)"
668
- },
669
- "use_count": {
670
- "type": "uint",
671
- "default": "0",
672
- "comment": "사용 횟수 캐시 (board_post_tag hook으로 갱신)"
673
- }
674
- }
675
- }
676
- ```
677
-
678
- ### 4.8 board_post_tag (게시글-태그 연결)
679
-
680
- `board_post`와 `board_tag`의 M:N 연결 테이블.
681
- 삽입/삭제 시 hook으로 `board_tag.use_count`를 갱신한다.
682
-
683
- ```json
684
- {
685
- "name": "board_post_tag",
686
- "description": "게시글-태그 연결 (M:N)",
687
- "fields": {
688
- "post_seq": {
689
- "index": true,
690
- "required": true,
691
- "type": "uint",
692
- "comment": "board_post seq"
693
- },
694
- "tag_seq": {
695
- "index": true,
696
- "required": true,
697
- "type": "uint",
698
- "comment": "board_tag seq"
699
- }
700
- },
701
- "unique": ["post_seq", "tag_seq"],
702
- "fk": {
703
- "post_seq": "board_post.seq",
704
- "tag_seq": "board_tag.seq"
705
- },
706
- "hooks": {
707
- "after_insert": [
708
- {
709
- "type": "update",
710
- "entity": "board_tag",
711
- "match": { "seq": "${new.tag_seq}" },
712
- "data": { "use_count": "${target.use_count + 1}" }
713
- }
714
- ],
715
- "after_delete": [
716
- {
717
- "type": "update",
718
- "entity": "board_tag",
719
- "match": { "seq": "${old.tag_seq}" },
720
- "data": { "use_count": "${target.use_count - 1}" }
721
- }
722
- ]
723
- }
724
- }
725
- ```
726
-
727
- ### 4.9 board_report (신고)
728
-
729
- 게시글·댓글에 대한 신고를 기록한다. `target_type + target_seq + user_seq` 복합 unique로 중복 신고를 방지한다.
730
-
731
- ```json
732
- {
733
- "name": "board_report",
734
- "description": "게시글·댓글 신고",
735
- "fields": {
736
- "target_type": {
737
- "type": ["post", "comment"],
738
- "required": true,
739
- "comment": "신고 대상 유형"
740
- },
741
- "target_seq": {
742
- "index": true,
743
- "required": true,
744
- "type": "uint",
745
- "comment": "대상 seq (board_post 또는 board_comment)"
746
- },
747
- "user_seq": {
748
- "index": true,
749
- "required": true,
750
- "type": "uint",
751
- "comment": "신고한 회원 seq"
752
- },
753
- "reason": {
754
- "type": ["spam", "abuse", "illegal", "other"],
755
- "required": true,
756
- "comment": "신고 사유"
757
- },
758
- "detail": {
759
- "comment": "신고 상세 내용 (선택)"
760
- },
761
- "status": {
762
- "type": ["pending", "resolved", "dismissed"],
763
- "default": "pending",
764
- "comment": "처리 상태. pending=미처리, resolved=처리완료, dismissed=기각"
765
- }
766
- },
767
- "unique": ["target_type", "target_seq", "user_seq"]
768
- }
769
- ```
770
-
771
- ### 4.10 board_read_log (읽음 표시)
772
-
773
- 회원이 게시글을 읽은 기록을 저장한다. `post_seq + user_seq` 복합 unique로 중복 기록을 방지한다.
774
- `read_time`을 업데이트 방식(upsert)으로 처리하여 목록 화면에서 새글 표시나 안 읽은 글 필터에 활용한다.
775
-
776
- ```json
777
- {
778
- "name": "board_read_log",
779
- "description": "게시글 읽음 기록",
780
- "fields": {
781
- "post_seq": {
782
- "index": true,
783
- "required": true,
784
- "type": "uint",
785
- "comment": "board_post seq"
786
- },
787
- "user_seq": {
788
- "index": true,
789
- "required": true,
790
- "type": "uint",
791
- "comment": "읽은 회원 seq"
792
- },
793
- "read_time": {
794
- "type": "datetime",
795
- "comment": "가장 최근에 읽은 시각 (upsert 시 갱신)"
796
- }
797
- },
798
- "unique": ["post_seq", "user_seq"],
799
- "fk": {
800
- "post_seq": "board_post.seq"
801
- }
802
- }
803
- ```
804
-
805
- ### 4.11 board_mention (멘션)
806
-
807
- 게시글·댓글 본문의 `@username` 언급을 파싱하여 저장한다.
808
- 삽입 hook으로 해당 회원에게 Push 알림을 발송한다.
809
-
810
- ```json
811
- {
812
- "name": "board_mention",
813
- "description": "게시글·댓글 멘션 기록",
814
- "fields": {
815
- "source_type": {
816
- "type": ["post", "comment"],
817
- "required": true,
818
- "comment": "멘션이 포함된 소스 유형"
819
- },
820
- "source_seq": {
821
- "index": true,
822
- "required": true,
823
- "type": "uint",
824
- "comment": "소스 seq (board_post 또는 board_comment)"
825
- },
826
- "mentioned_user_seq": {
827
- "index": true,
828
- "required": true,
829
- "type": "uint",
830
- "comment": "멘션된 회원 seq"
831
- },
832
- "is_read": {
833
- "type": ["Y", "N"],
834
- "default": "N",
835
- "comment": "알림 읽음 여부"
836
- }
837
- },
838
- "unique": ["source_type", "source_seq", "mentioned_user_seq"],
839
- "hooks": {
840
- "after_insert": [
841
- {
842
- "type": "push",
843
- "target": "user:${new.mentioned_user_seq}",
844
- "title": "회원이 나를 멘션했습니다",
845
- "body": "${new.source_type} #${new.source_seq}",
846
- "data": {
847
- "type": "mention",
848
- "source_type": "${new.source_type}",
849
- "source_seq": "${new.source_seq}"
850
- }
851
- }
852
- ]
853
- }
854
- }
855
- ```
856
-
857
- ---
858
-
859
- ### 엔티티 관계도
860
-
861
- ```
862
- board_category
863
-
864
- │ 1:N
865
-
866
- board_post ◀──────────────────── board_post ─── user
867
- │ parent_seq (자기참조) (답글, depth≥1) │
868
- │ root_seq (원글 직추적) │ (user_seq FK)
869
- │ │
870
- │ 1:N (post_seq FK) │
871
- ├──▶ board_comment ────────────────────────────────┘
872
- │ (post_seq → 대상 글, root_seq → 원글)
873
-
874
- │ 1:N (post_seq FK)
875
- ├──▶ board_like (post_seq + user_seq unique)
876
-
877
-
878
- │ 1:N (target_type='comment', target_seq=comment.seq)
879
- ├──▶ board_rating (target_type='comment', target_seq=comment.seq)
880
-
881
- │ 1:N (post_seq FK)
882
- ├──▶ board_rating (target_type='post', target_seq=post.seq, target_type+target_seq+user_seq unique, score 1~5)
883
-
884
- │ 1:N (post_seq FK)
885
- ├──▶ board_post_tag ────▶ board_tag (name unique, use_count 캐시)
886
-
887
- │ 1:N (target_type='post', target_seq=post.seq)
888
- ├──▶ board_report (target_type+target_seq+user_seq unique, status: pending/resolved/dismissed)
889
-
890
- │ 1:N (target_type='comment', target_seq=comment.seq)
891
- ├──▶ board_report (댓글 신고)
892
-
893
- │ 1:N (post_seq FK)
894
- ├──▶ board_read_log (post_seq + user_seq unique, read_time 갱신)
895
-
896
- │ 1:N (source_type='post', source_seq=post.seq)
897
- ├──▶ board_mention (source_type+source_seq+mentioned_user_seq unique, hook → push)
898
-
899
- │ 1:N (entity_seq FK, 엔티티 서버 files API)
900
- └──▶ file_meta ← /v1/files/board_post/upload?entity_seq={seq}
901
- ```
902
-
903
- ---
904
-
905
- ## 5. 앱서버 Routes 설계
906
-
907
- ### 5.1 라우트 구조
908
-
909
- `src/app/routes/board/` 디렉토리에 Fastify 라우트로 구현한다.
910
-
911
- ```typescript
912
- // src/app/routes/board/route.ts
913
- import type { FastifyInstance } from "fastify";
914
- import * as posts from "./handlers/posts.ts";
915
- import * as comments from "./handlers/comments.ts";
916
- import * as files from "./handlers/files.ts";
917
- import * as categories from "./handlers/categories.ts";
918
- import * as likes from "./handlers/likes.ts";
919
- import * as ratings from "./handlers/ratings.ts";
920
- import * as tags from "./handlers/tags.ts";
921
- import * as reports from "./handlers/reports.ts";
922
- import * as mentions from "./handlers/mentions.ts";
923
-
924
- export default async function boardRoutes(app: FastifyInstance) {
925
- // ── 카테고리 ──
926
- app.get("/categories", categories.list);
927
- app.get("/categories/:seq", categories.detail);
928
- app.post(
929
- "/categories",
930
- { preHandler: app.authenticate },
931
- categories.create,
932
- );
933
- app.put(
934
- "/categories/:seq",
935
- { preHandler: app.authenticate },
936
- categories.update,
937
- );
938
- app.delete(
939
- "/categories/:seq",
940
- { preHandler: app.authenticate },
941
- categories.remove,
942
- );
943
-
944
- // ── 게시글 · 답글 (board_post 통합) ──
945
- app.get("/:category/list", posts.list);
946
- app.get("/posts/:seq", posts.detail);
947
- app.post(
948
- "/:category/submit",
949
- { preHandler: app.authenticate },
950
- posts.create,
951
- );
952
- app.put("/posts/:seq", { preHandler: app.authenticate }, posts.update);
953
- app.delete("/posts/:seq", { preHandler: app.authenticate }, posts.remove);
954
-
955
- // ── 댓글 (post_seq = 원글 또는 답글 board_post seq) ──
956
- app.get("/posts/:postSeq/comments", comments.list);
957
- app.post(
958
- "/posts/:postSeq/comments/submit",
959
- { preHandler: app.authenticate },
960
- comments.create,
961
- );
962
- app.put(
963
- "/comments/:seq",
964
- { preHandler: app.authenticate },
965
- comments.update,
966
- );
967
- app.delete(
968
- "/comments/:seq",
969
- { preHandler: app.authenticate },
970
- comments.remove,
971
- );
972
-
973
- // ── 파일 (엔티티 서버 files API 프록시) ──
974
- app.get("/posts/:postSeq/files", files.list);
975
- app.post(
976
- "/posts/:postSeq/files",
977
- { preHandler: app.authenticate },
978
- files.upload,
979
- );
980
- app.delete("/files/:uuid", { preHandler: app.authenticate }, files.remove);
981
- // 다운로드는 엔티티 서버 /v1/files/{uuid} 로 직접 리다이렉트 또는 프록시
982
- app.get("/files/:uuid", files.view);
983
-
984
- // ── 비회원 글작성 ──
985
- app.post("/:category/guest-submit", posts.guestCreate);
986
- app.post("/posts/:seq/guest-auth", posts.guestAuth); // 비밀번호 확인 → 임시 토큰 반환
987
-
988
- // ── 좋아요 토글 (이미 좋아요 → 취소, 없으면 추가) ──
989
- app.post(
990
- "/posts/:seq/like",
991
- { preHandler: app.authenticate },
992
- likes.toggle,
993
- );
994
-
995
- // ── 답변 채택 (원글 작성자 또는 관리자만) ──
996
- app.post(
997
- "/posts/:seq/accept",
998
- { preHandler: app.authenticate },
999
- posts.accept,
1000
- );
1001
-
1002
- // ── 별점 등록 / 수정 (게시글·답글 또는 댓글) ──
1003
- app.post(
1004
- "/posts/:seq/rating",
1005
- { preHandler: app.authenticate },
1006
- ratings.upsert,
1007
- );
1008
- app.post(
1009
- "/comments/:seq/rating",
1010
- { preHandler: app.authenticate },
1011
- ratings.upsert,
1012
- );
1013
-
1014
- // ── 태그 ──
1015
- app.get("/tags", tags.list);
1016
- app.put(
1017
- "/posts/:seq/tags",
1018
- { preHandler: app.authenticate },
1019
- tags.setPostTags,
1020
- );
1021
-
1022
- // ── 신고 ──
1023
- app.post(
1024
- "/posts/:seq/report",
1025
- { preHandler: app.authenticate },
1026
- reports.submit,
1027
- );
1028
- app.post(
1029
- "/comments/:seq/report",
1030
- { preHandler: app.authenticate },
1031
- reports.submit,
1032
- );
1033
-
1034
- // ── 읽음 표시 ──
1035
- app.post(
1036
- "/posts/:seq/read",
1037
- { preHandler: app.authenticate },
1038
- posts.markRead,
1039
- );
1040
-
1041
- // ── 멘션 ──
1042
- app.get("/mentions", { preHandler: app.authenticate }, mentions.list);
1043
- app.patch(
1044
- "/mentions/:seq/read",
1045
- { preHandler: app.authenticate },
1046
- mentions.markRead,
1047
- );
1048
- }
1049
- ```
1050
-
1051
- > 모든 엔드포인트는 `/api/v1/board/*` 으로 매핑된다 (폴더명 기반 자동 prefix).
1052
-
1053
- ### 5.2 핸들러 디렉토리 구조
1054
-
1055
- ```
1056
- src/app/routes/board/
1057
- ├── route.ts # 라우트 정의
1058
- └── handlers/
1059
- ├── posts.ts # 게시글 CRUD + 비회원 작성/인증 + 답변 채택 + 읽음 표시
1060
- ├── comments.ts # 댓글 CRUD
1061
- ├── files.ts # 파일 — entityServer.fileUpload/fileList/fileDelete 사용
1062
- ├── likes.ts # 좋아요 토글 (board_like)
1063
- ├── ratings.ts # 별점 등록/수정 (board_rating)
1064
- ├── tags.ts # 태그 목록 조회 / 게시글 태그 일괄 설정 (board_tag, board_post_tag)
1065
- ├── reports.ts # 신고 제출 (board_report) — 게시글·댓글 공용
1066
- ├── mentions.ts # 멘션 목록/읽음 처리 (board_mention)
1067
- └── categories.ts # 카테고리 관리
1068
- ```
1069
-
1070
- ### 5.3 핸들러 구현 예시
1071
-
1072
- ```typescript
1073
- // src/app/routes/board/handlers/posts.ts
1074
- import { FastifyRequest, FastifyReply } from "fastify";
1075
- import { ok, fail, entityServer } from "@gateway/api";
1076
-
1077
- export async function list(
1078
- req: FastifyRequest<{
1079
- Params: { category: string };
1080
- Querystring: {
1081
- page?: number;
1082
- per_page?: number;
1083
- search?: string;
1084
- root_seq?: number; // root_seq 필터: 특정 원글의 답글만 조회
1085
- depth?: number; // depth 필터: 0=원글만, 생략=전체
1086
- };
1087
- }>,
1088
- reply: FastifyReply,
1089
- ) {
1090
- const { category } = req.params;
1091
- const { page = 1, per_page = 20, search, root_seq, depth } = req.query;
1092
-
1093
- const cat = await entityServer.find("board_category", {
1094
- name: category,
1095
- status: "active",
1096
- });
1097
- if (cat.count === 0)
1098
- return reply.send(fail("카테고리를 찾을 수 없습니다."));
1099
-
1100
- const filter: Record<string, unknown> = {
1101
- category_seq: cat.items[0].seq,
1102
- status: "published",
1103
- };
1104
- if (root_seq !== undefined) filter.root_seq = root_seq;
1105
- if (depth !== undefined) filter.depth = depth;
1106
-
1107
- const resp = await entityServer.list("board_post", {
1108
- filter,
1109
- search,
1110
- page,
1111
- limit: per_page,
1112
- sort: "created_time",
1113
- order: "desc",
1114
- });
1115
-
1116
- return reply.send(ok(resp));
1117
- }
1118
-
1119
- export async function create(
1120
- req: FastifyRequest<{
1121
- Params: { category: string };
1122
- Body: {
1123
- title: string;
1124
- content: string;
1125
- parent_seq?: number; // 답글일 때 설정
1126
- pinned?: string;
1127
- status?: string; // "published" | "draft"
1128
- tags?: string[]; // 태그 name 배열
1129
- };
1130
- }>,
1131
- reply: FastifyReply,
1132
- ) {
1133
- const { category } = req.params;
1134
- const { title, content, parent_seq, pinned, status, tags } = req.body;
1135
- const userSeq = req.user.account_seq;
1136
-
1137
- const cat = await entityServer.find("board_category", {
1138
- name: category,
1139
- status: "active",
1140
- });
1141
- if (cat.count === 0)
1142
- return reply.send(fail("카테고리를 찾을 수 없습니다."));
1143
-
1144
- const catItem = cat.items[0];
1145
-
1146
- // pinned는 관리자만 설정 가능
1147
- if (pinned === "Y" && !req.user.is_admin)
1148
- return reply.send(fail("글 고정은 관리자만 가능합니다."));
1149
-
1150
- // 답글일 때 root_seq, depth 자동 가산
1151
- let root_seq: number | undefined;
1152
- let depth = 0;
1153
- if (parent_seq) {
1154
- const parent = await entityServer.find("board_post", {
1155
- seq: parent_seq,
1156
- });
1157
- if (parent.count === 0)
1158
- return reply.send(fail("부모 글을 찾을 수 없습니다."));
1159
- const p = parent.items[0];
1160
- root_seq = p.root_seq ?? p.seq;
1161
- depth = (p.depth ?? 0) + 1;
1162
- }
1163
-
1164
- // 익명 게시판이면 author_name 강제 치환
1165
- const isAnonymous = catItem.anonymous_enabled === "Y";
1166
- const authorName = isAnonymous ? "익명" : req.user.name;
1167
-
1168
- const resp = await entityServer.submit("board_post", {
1169
- category_seq: catItem.seq,
1170
- user_seq: userSeq,
1171
- author_name: authorName,
1172
- title,
1173
- content,
1174
- status: status || "published",
1175
- parent_seq: parent_seq ?? null,
1176
- root_seq: root_seq ?? null, // 원글은 insert 후 seq로 갱신 필요
1177
- depth,
1178
- pinned: parent_seq ? "N" : pinned || "N",
1179
- });
1180
-
1181
- // 원글이면 root_seq = 자기 seq
1182
- if (!parent_seq) {
1183
- await entityServer.submit("board_post", {
1184
- seq: resp.seq,
1185
- root_seq: resp.seq,
1186
- });
1187
- }
1188
-
1189
- // 태그 연결
1190
- if (tags && tags.length > 0) {
1191
- await setPostTags(resp.seq, tags);
1192
- }
1193
-
1194
- // @username 멘션 파싱 및 board_mention 삽입
1195
- const mentionPattern = /@([a-zA-Z0-9_]+)/g;
1196
- let match: RegExpExecArray | null;
1197
- while ((match = mentionPattern.exec(content)) !== null) {
1198
- const username = match[1];
1199
- const user = await entityServer.find("user", { username });
1200
- if (user.count > 0) {
1201
- await entityServer.submit("board_mention", {
1202
- source_type: "post",
1203
- source_seq: resp.seq,
1204
- mentioned_user_seq: user.items[0].seq,
1205
- });
1206
- }
1207
- }
1208
-
1209
- return reply.code(201).send(ok({ seq: resp.seq }));
1210
- }
1211
- ```
1212
-
1213
- ### 5.4 비회원 글작성 핸들러 예시
1214
-
1215
- ```typescript
1216
- // src/app/routes/board/handlers/posts.ts (추가 함수)
1217
- import bcrypt from "bcrypt";
1218
-
1219
- export async function guestCreate(
1220
- req: FastifyRequest<{
1221
- Params: { category: string };
1222
- Body: {
1223
- title: string;
1224
- content: string;
1225
- guest_name: string;
1226
- guest_password: string; // 평문 — 핸들러에서 bcrypt 해시 처리
1227
- };
1228
- }>,
1229
- reply: FastifyReply,
1230
- ) {
1231
- const { category } = req.params;
1232
- const { title, content, guest_name, guest_password } = req.body;
1233
-
1234
- const cat = await entityServer.find("board_category", {
1235
- name: category,
1236
- status: "active",
1237
- });
1238
- if (cat.count === 0)
1239
- return reply.send(fail("카테고리를 찾을 수 없습니다."));
1240
-
1241
- const catItem = cat.items[0];
1242
- if (catItem.guest_write_enabled !== "Y")
1243
- return reply.send(
1244
- fail("이 게시판은 비회원 글작성이 허용되지 않습니다."),
1245
- );
1246
-
1247
- const hashed = await bcrypt.hash(guest_password, 10);
1248
-
1249
- const resp = await entityServer.submit("board_post", {
1250
- category_seq: catItem.seq,
1251
- user_seq: null, // 비회원
1252
- author_name: guest_name,
1253
- guest_password: hashed,
1254
- title,
1255
- content,
1256
- depth: 0,
1257
- });
1258
-
1259
- // root_seq = 자기 seq
1260
- await entityServer.submit("board_post", {
1261
- seq: resp.seq,
1262
- root_seq: resp.seq,
1263
- });
1264
-
1265
- return reply.code(201).send(ok({ seq: resp.seq }));
1266
- }
1267
-
1268
- export async function guestAuth(
1269
- req: FastifyRequest<{
1270
- Params: { seq: string };
1271
- Body: { guest_password: string };
1272
- }>,
1273
- reply: FastifyReply,
1274
- ) {
1275
- const seq = Number(req.params.seq);
1276
- const { guest_password } = req.body;
1277
-
1278
- const post = await entityServer.find("board_post", { seq });
1279
- if (post.count === 0) return reply.send(fail("게시글을 찾을 수 없습니다."));
1280
-
1281
- const item = post.items[0];
1282
- if (!item.guest_password)
1283
- return reply.send(fail("비회원 게시글이 아닙니다."));
1284
-
1285
- const ok_ = await bcrypt.compare(guest_password, item.guest_password);
1286
- if (!ok_) return reply.send(fail("비밀번호가 일치하지 않습니다."));
1287
-
1288
- // 임시 허가 토큰 (서명된 JWT, 만료 30분)
1289
- const token = await reply.jwtSign(
1290
- { guest_post_seq: seq },
1291
- { expiresIn: "30m" },
1292
- );
1293
- return reply.send(ok({ token }));
1294
- }
1295
-
1296
- export async function accept(
1297
- req: FastifyRequest<{ Params: { seq: string } }>,
1298
- reply: FastifyReply,
1299
- ) {
1300
- const seq = Number(req.params.seq);
1301
- const userSeq = req.user.account_seq;
1302
-
1303
- const post = await entityServer.find("board_post", { seq });
1304
- if (post.count === 0) return reply.send(fail("게시글을 찾을 수 없습니다."));
1305
- const item = post.items[0];
1306
-
1307
- // 원글은 채택할 수 없다 (답글만 채택 가능)
1308
- if (item.parent_seq === null)
1309
- return reply.send(
1310
- fail("원글은 채택할 수 없습니다. 답글만 채택 가능합니다."),
1311
- );
1312
-
1313
- // 원글 작성자 또는 관리자만 채택 가능
1314
- // 원글(root)의 작성자를 확인해야 하므로 root_seq로 원글 조회
1315
- const rootPost = await entityServer.find("board_post", {
1316
- seq: item.root_seq,
1317
- });
1318
- if (rootPost.count === 0)
1319
- return reply.send(fail("원글을 찾을 수 없습니다."));
1320
- if (rootPost.items[0].user_seq !== userSeq && !req.user.is_admin)
1321
- return reply.send(fail("채택 권한이 없습니다."));
1322
-
1323
- // 같은 root_seq 내 기존 채택 해제
1324
- if (item.root_seq) {
1325
- const prevAccepted = await entityServer.find("board_post", {
1326
- root_seq: item.root_seq,
1327
- accepted: "Y",
1328
- });
1329
- for (const p of prevAccepted.items) {
1330
- await entityServer.submit("board_post", {
1331
- seq: p.seq,
1332
- accepted: "N",
1333
- });
1334
- }
1335
- }
1336
-
1337
- await entityServer.submit("board_post", { seq, accepted: "Y" });
1338
- return reply.send(ok(null));
1339
- }
1340
- ```
1341
-
1342
- ```typescript
1343
- // src/app/routes/board/handlers/likes.ts
1344
- import type { FastifyRequest, FastifyReply } from "fastify";
1345
- import { ok, fail, entityServer } from "@gateway/api";
1346
-
1347
- export async function toggle(
1348
- req: FastifyRequest<{ Params: { seq: string } }>,
1349
- reply: FastifyReply,
1350
- ) {
1351
- const postSeq = Number(req.params.seq);
1352
- const userSeq = req.user.account_seq;
1353
-
1354
- const existing = await entityServer.find("board_like", {
1355
- post_seq: postSeq,
1356
- user_seq: userSeq,
1357
- });
1358
-
1359
- if (existing.count > 0) {
1360
- // 이미 좋아요 → 취소
1361
- await entityServer.remove("board_like", { seq: existing.items[0].seq });
1362
- return reply.send(ok({ liked: false }));
1363
- } else {
1364
- // 좋아요 추가
1365
- await entityServer.submit("board_like", {
1366
- post_seq: postSeq,
1367
- user_seq: userSeq,
1368
- });
1369
- return reply.send(ok({ liked: true }));
1370
- }
1371
- }
1372
- ```
1373
-
1374
- ```typescript
1375
- // src/app/routes/board/handlers/tags.ts
1376
- import type { FastifyRequest, FastifyReply } from "fastify";
1377
- import { ok, fail, entityServer } from "@gateway/api";
1378
-
1379
- export async function list(
1380
- req: FastifyRequest<{
1381
- Querystring: { search?: string; limit?: number };
1382
- }>,
1383
- reply: FastifyReply,
1384
- ) {
1385
- const { search, limit = 20 } = req.query;
1386
- const resp = await entityServer.list("board_tag", {
1387
- search,
1388
- limit,
1389
- sort: "use_count",
1390
- order: "desc",
1391
- });
1392
- return reply.send(ok(resp));
1393
- }
1394
-
1395
- export async function setPostTags(
1396
- req: FastifyRequest<{
1397
- Params: { seq: string };
1398
- Body: { tags: string[] }; // tag name(슬러그) 배열
1399
- }>,
1400
- reply: FastifyReply,
1401
- ) {
1402
- const postSeq = Number(req.params.seq);
1403
- const { tags: tagNames } = req.body;
1404
-
1405
- // 기존 board_post_tag 전체 삭제 (replace 방식)
1406
- const existing = await entityServer.list("board_post_tag", {
1407
- filter: { post_seq: postSeq },
1408
- limit: 100,
1409
- });
1410
- for (const pt of existing.items) {
1411
- await entityServer.remove("board_post_tag", { seq: pt.seq });
1412
- }
1413
-
1414
- const result: string[] = [];
1415
- for (const name of tagNames) {
1416
- const slug = name.toLowerCase().replace(/\s+/g, "-");
1417
- // 태그 조회 또는 자동 생성
1418
- let tagRes = await entityServer.find("board_tag", { name: slug });
1419
- if (tagRes.count === 0) {
1420
- const created = await entityServer.submit("board_tag", {
1421
- name: slug,
1422
- label: name,
1423
- });
1424
- tagRes = await entityServer.find("board_tag", { seq: created.seq });
1425
- }
1426
- await entityServer.submit("board_post_tag", {
1427
- post_seq: postSeq,
1428
- tag_seq: tagRes.items[0].seq,
1429
- });
1430
- result.push(slug);
1431
- }
1432
-
1433
- return reply.send(ok({ tags: result }));
1434
- }
1435
- ```
1436
-
1437
- ```typescript
1438
- // src/app/routes/board/handlers/ratings.ts
1439
- import type { FastifyRequest, FastifyReply } from "fastify";
1440
- import { ok, fail, entityServer } from "@gateway/api";
1441
-
1442
- /**
1443
- * /posts/:seq/rating → target_type: "post"
1444
- * /comments/:seq/rating → target_type: "comment"
1445
- * routeConfig.url 에서 target_type을 판별한다.
1446
- */
1447
- export async function upsert(
1448
- req: FastifyRequest<{
1449
- Params: { seq: string };
1450
- Body: { score: number }; // 1~5
1451
- }>,
1452
- reply: FastifyReply,
1453
- ) {
1454
- const targetSeq = Number(req.params.seq);
1455
- const userSeq = req.user.account_seq;
1456
- const { score } = req.body;
1457
-
1458
- const targetType = req.routeOptions.url.includes("/comments/")
1459
- ? "comment"
1460
- : "post";
1461
-
1462
- if (score < 1 || score > 5)
1463
- return reply.send(fail("별점은 1~5 사이여야 합니다."));
1464
-
1465
- const existing = await entityServer.find("board_rating", {
1466
- target_type: targetType,
1467
- target_seq: targetSeq,
1468
- user_seq: userSeq,
1469
- });
1470
-
1471
- if (existing.count > 0) {
1472
- // 기존 별점 수정 → after_update hook이 rating_sum 보정
1473
- await entityServer.submit("board_rating", {
1474
- seq: existing.items[0].seq,
1475
- score: String(score),
1476
- });
1477
- return reply.send(ok({ updated: true }));
1478
- } else {
1479
- await entityServer.submit("board_rating", {
1480
- target_type: targetType,
1481
- target_seq: targetSeq,
1482
- user_seq: userSeq,
1483
- score: String(score),
1484
- });
1485
- return reply.send(ok({ created: true }));
1486
- }
1487
- }
1488
- ```
1489
-
1490
- ```typescript
1491
- // src/app/routes/board/handlers/reports.ts
1492
- import type { FastifyRequest, FastifyReply } from "fastify";
1493
- import { ok, fail, entityServer } from "@gateway/api";
1494
-
1495
- /**
1496
- * POST /posts/:seq/report → target_type: "post"
1497
- * POST /comments/:seq/report → target_type: "comment"
1498
- * routeOptions.url 에서 target_type을 판별한다.
1499
- */
1500
- export async function submit(
1501
- req: FastifyRequest<{
1502
- Params: { seq: string };
1503
- Body: {
1504
- reason: "spam" | "abuse" | "illegal" | "other";
1505
- detail?: string;
1506
- };
1507
- }>,
1508
- reply: FastifyReply,
1509
- ) {
1510
- const targetSeq = Number(req.params.seq);
1511
- const userSeq = req.user.account_seq;
1512
- const { reason, detail } = req.body;
1513
-
1514
- const targetType = req.routeOptions.url.includes("/comments/")
1515
- ? "comment"
1516
- : "post";
1517
-
1518
- // 대상 존재 여부 확인 (없으면 404)
1519
- const targetEntity =
1520
- targetType === "comment" ? "board_comment" : "board_post";
1521
- const target = await entityServer.find(targetEntity, { seq: targetSeq });
1522
- if (target.count === 0)
1523
- return reply.code(404).send(fail("신고 대상을 찾을 수 없습니다."));
1524
-
1525
- // 동일 대상에 대한 중복 신고 방지
1526
- const existing = await entityServer.find("board_report", {
1527
- target_type: targetType,
1528
- target_seq: targetSeq,
1529
- user_seq: userSeq,
1530
- });
1531
- if (existing.count > 0) return reply.send(fail("이미 신고한 대상입니다."));
1532
-
1533
- await entityServer.submit("board_report", {
1534
- target_type: targetType,
1535
- target_seq: targetSeq,
1536
- user_seq: userSeq,
1537
- reason,
1538
- detail: detail ?? null,
1539
- });
1540
-
1541
- return reply.code(201).send(ok({ reported: true }));
1542
- }
1543
- ```
1544
-
1545
- ```typescript
1546
- // src/app/routes/board/handlers/mentions.ts
1547
- import type { FastifyRequest, FastifyReply } from "fastify";
1548
- import { ok, fail, entityServer } from "@gateway/api";
1549
-
1550
- export async function list(
1551
- req: FastifyRequest<{ Querystring: { is_read?: "Y" | "N" } }>,
1552
- reply: FastifyReply,
1553
- ) {
1554
- const userSeq = req.user.account_seq;
1555
- const { is_read } = req.query;
1556
-
1557
- const filter: Record<string, unknown> = { mentioned_user_seq: userSeq };
1558
- if (is_read) filter.is_read = is_read;
1559
-
1560
- const resp = await entityServer.list("board_mention", {
1561
- filter,
1562
- sort: "created_time",
1563
- order: "desc",
1564
- });
1565
- return reply.send(ok(resp));
1566
- }
1567
-
1568
- export async function markRead(
1569
- req: FastifyRequest<{ Params: { seq: string } }>,
1570
- reply: FastifyReply,
1571
- ) {
1572
- const seq = Number(req.params.seq);
1573
-
1574
- const mention = await entityServer.find("board_mention", { seq });
1575
- if (mention.count === 0)
1576
- return reply.code(404).send(fail("멘션을 찾을 수 없습니다."));
1577
- if (mention.items[0].mentioned_user_seq !== req.user.account_seq)
1578
- return reply.code(403).send(fail("본인 멘션만 처리할 수 있습니다."));
1579
-
1580
- await entityServer.submit("board_mention", { seq, is_read: "Y" });
1581
- return reply.send(ok({ read: true }));
1582
- }
1583
- ```
1584
-
1585
- > **익명 처리**: `posts.ts` create/guestCreate 핸들러에서 카테고리의 `anonymous_enabled = "Y"` 여부를 확인하여
1586
- > `author_name`을 `"익명"`으로 강제 저장하고, 응답 시 `user_seq`를 `null`로 마스킹한다.
1587
- > 댓글도 동일하게 익명 게시판이면 `author_name = "익명"`, 응답 시 `user_seq = null` 마스킹.
1588
- > `user_seq`는 DB에는 실제 값으로 저장되며, 관리자의 신고 처리와 본인 확인에만 내부적으로 사용된다.
1589
-
1590
- > **읽음 표시**: `posts.ts markRead` 핸들러에서 `board_read_log`를 upsert(`post_seq + user_seq` unique)하여
1591
- > `read_time`을 현재 시각으로 갱신한다. 상세 조회(GET /posts/:seq) 시 자동으로 호출할 수도 있고,
1592
- > 클라이언트에서 명시적으로 `POST /posts/:seq/read`를 호출하는 방식 중 카테고리 설정으로 선택한다.
1593
-
1594
- > **멘션 처리**: `comments.ts create` 및 `posts.ts create` 핸들러에서 `content` 내 `@username` 패턴을
1595
- > 정규식으로 파싱하고, 해당 username을 가진 회원을 조회한 후 `board_mention`을 삽입한다.
1596
- > 삽입 hook이 자동으로 Push 알림을 발송하므로 핸들러에서 별도 처리는 필요 없다.
1597
- >
1598
- > **멘션 수정 시 정리 규칙**: 글/댓글 수정 시 멘션을 재파싱하며, 기존 멘션 중 수정 후 본문에 없는 것은 삭제,
1599
- > 새로 추가된 멘션만 `board_mention`에 삽입한다 (diff 방식). 이미 읽은 멘션의 삭제는 문제없다.
1600
-
1601
- ### 5.5 앱서버 핸들러 역할
1602
-
1603
- | 역할 | 설명 |
1604
- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------- |
1605
- | **인증** | `preHandler: app.authenticate` — JWT 검증 |
1606
- | **입력 검증** | title 길이, content HTML sanitize 등 |
1607
- | **권한 확인** | 카테고리별 read_role/write_role 대조, 본인 확인 |
1608
- | **Entity Server 호출** | `entityServer.*` SDK로 CRUD 위임 |
1609
- | **응답 가공** | `ok()`, `fail()` 래퍼로 응답 형식 통일 |
1610
- | **조회수 처리** | `view_count_mode`에 따라 카운트 증가. `daily`(기본): Redis TTL 86400s 키로 중복 방지. 회원은 `user_seq`, 비회원은 IP 기반 |
1611
-
1612
- ---
1613
-
1614
- ## 6. API 상세 스펙
1615
-
1616
- 모든 엔드포인트의 base path: `/api/v1/board`
1617
-
1618
- ### 6.1 게시글 목록 조회
1619
-
1620
- ```
1621
- GET /api/v1/board/{category}/list?page=1&per_page=20&search={keyword}&sort=created_time&order=desc&root_seq={seq}&depth={n}
1622
- ```
1623
-
1624
- **Query Parameters:**
1625
-
1626
- | 파라미터 | 타입 | 기본값 | 설명 |
1627
- | -------- | ------ | ------------ | --------------------------------------------- |
1628
- | category | string | (path) | 카테고리 name (URL path) |
1629
- | page | int | 1 | 페이지 번호 |
1630
- | per_page | int | 20 | 페이지당 건수 |
1631
- | search | string | - | 제목+내용 통합 검색 |
1632
- | sort | string | created_time | 정렬 기준 |
1633
- | order | string | desc | asc / desc |
1634
- | pinned | string | - | Y: 고정글만 |
1635
- | root_seq | int | - | 특정 원글의 답글만 조회 (트리 펼치기) |
1636
- | depth | int | - | 0: 원글만, 생략: 전체 (원글+답글 flat 목록) |
1637
- | status | string | published | `draft`: 내 임시저장 목록 (인증 필요, 본인만) |
1638
-
1639
- > **주의**: 기본 목록(`depth` 생략)은 원글과 답글을 **모두 flat하게** 반환한다.
1640
- > 프론트엔드에서 `parent_seq`와 `depth`를 이용해 트리 구조로 조립한다.
1641
- > `status=draft` 조회 시 앱서버 핸들러에서 `user_seq: req.user.account_seq` 필터를 강제하여 본인 임시저장만 반환한다.
1642
-
1643
- **Response:**
1644
-
1645
- ```json
1646
- {
1647
- "success": true,
1648
- "data": {
1649
- "items": [
1650
- {
1651
- "seq": 42,
1652
- "category_seq": 1,
1653
- "user_seq": 10,
1654
- "author_name": "홍길동",
1655
- "title": "공지사항입니다",
1656
- "status": "published",
1657
- "pinned": "Y",
1658
- "parent_seq": null,
1659
- "root_seq": 42,
1660
- "depth": 0,
1661
- "view_count": 123,
1662
- "reply_count": 3,
1663
- "comment_count": 5,
1664
- "like_count": 12,
1665
- "rating_sum": 18,
1666
- "rating_count": 5,
1667
- "accepted": "N",
1668
- "file_count": 2,
1669
- "tags": ["javascript", "vue"],
1670
- "created_time": "2026-03-06T09:00:00Z",
1671
- "updated_time": "2026-03-06T09:00:00Z"
1672
- },
1673
- {
1674
- "seq": 10,
1675
- "category_seq": 1,
1676
- "user_seq": 11,
1677
- "author_name": "김철수",
1678
- "title": "답변드립니다",
1679
- "status": "published",
1680
- "pinned": "N",
1681
- "parent_seq": 42,
1682
- "root_seq": 42,
1683
- "depth": 1,
1684
- "view_count": 20,
1685
- "reply_count": 0,
1686
- "comment_count": 2,
1687
- "like_count": 3,
1688
- "rating_sum": 4,
1689
- "rating_count": 1,
1690
- "accepted": "Y",
1691
- "file_count": 0,
1692
- "tags": [],
1693
- "created_time": "2026-03-06T10:00:00Z",
1694
- "updated_time": "2026-03-06T10:00:00Z"
1695
- }
1696
- ],
1697
- "pagination": {
1698
- "page": 1,
1699
- "per_page": 20,
1700
- "total": 156,
1701
- "total_pages": 8
1702
- }
1703
- }
1704
- }
1705
- ```
1706
-
1707
- > **참고**: 목록 조회 시 `content`는 제외하여 전송량을 줄인다.
1708
- > 원글(`parent_seq=null`)과 답글이 flat하게 섞여 있으므로, 프론트엔드에서 `parent_seq`로 트리 조립.
1709
-
1710
- ### 6.2 게시글 상세 조회
1711
-
1712
- ```
1713
- GET /api/v1/board/posts/{seq}
1714
- ```
1715
-
1716
- **Response:**
1717
-
1718
- ```json
1719
- {
1720
- "success": true,
1721
- "data": {
1722
- "seq": 42,
1723
- "category_seq": 1,
1724
- "user_seq": 10,
1725
- "author_name": "홍길동",
1726
- "title": "공지사항입니다",
1727
- "content": "<p>본문 내용...</p>",
1728
- "status": "published",
1729
- "pinned": "Y",
1730
- "parent_seq": null,
1731
- "root_seq": 42,
1732
- "depth": 0,
1733
- "view_count": 124,
1734
- "reply_count": 3,
1735
- "comment_count": 5,
1736
- "like_count": 12,
1737
- "rating_sum": 18,
1738
- "rating_count": 5,
1739
- "accepted": "N",
1740
- "file_count": 2,
1741
- "created_time": "2026-03-06T09:00:00Z",
1742
- "updated_time": "2026-03-06T09:00:00Z",
1743
- "files": [
1744
- {
1745
- "uuid": "550e8400-e29b-41d4-a716-446655440000",
1746
- "original_name": "첨부자료.pdf",
1747
- "mime_type": "application/pdf",
1748
- "size": 204800,
1749
- "account_seq": 10,
1750
- "created_time": "2026-03-06T09:00:00Z"
1751
- }
1752
- ],
1753
- "tags": ["javascript", "vue"],
1754
- "is_mine": true
1755
- }
1756
- }
1757
- ```
1758
-
1759
- > 상세 조회 시 앱서버에서 `view_count_mode`에 따라 조건부로 `view_count`를 +1 갱신 후 응답한다.
1760
- >
1761
- > | mode | 동작 |
1762
- > | --------- | ---------------------------------------------------------------------- |
1763
- > | `always` | 매 요청마다 증가 |
1764
- > | `session` | 요청 헤더의 세션/쿠키 기준, 없으면 설정 후 1회 증가 |
1765
- > | `daily` | Redis `view:{postSeq}:{userSeq\|ip}` 키 TTL 86400s — 키 없을 때만 증가 |
1766
- > | `once` | Redis `view:{postSeq}:{userSeq\|ip}` 키 영구 저장 — 키 없을 때만 증가 |
1767
-
1768
- ### 6.3 게시글 작성 (원글 · 답글 공통)
1769
-
1770
- ```
1771
- POST /api/v1/board/{category}/submit
1772
- Content-Type: application/json
1773
-
1774
- {
1775
- "title": "공지사항입니다",
1776
- "content": "<p>본문 내용</p>",
1777
- "parent_seq": null,
1778
- "pinned": "N",
1779
- "status": "published",
1780
- "tags": ["javascript", "vue"]
1781
- }
1782
- ```
1783
-
1784
- > `parent_seq`가 null이면 원글, 값이 있으면 답글로 처리된다.
1785
- > 앱서버에서 부모 글의 `root_seq`, `depth`를 조회하여 자동 계산한다.
1786
- > `status: "draft"` 로 전송하면 임시저장되며, 목록에 노출되지 않는다.
1787
- > `tags` 배열이 있으면 `setPostTags`를 내부 호출하여 태그를 연결한다 (없으면 태그 변경 없음).
1788
-
1789
- ### 6.4 게시글 수정
1790
-
1791
- ```
1792
- PUT /api/v1/board/posts/{seq}
1793
- Content-Type: application/json
1794
-
1795
- {
1796
- "title": "수정된 제목",
1797
- "content": "<p>수정된 본문</p>",
1798
- "status": "published",
1799
- "tags": ["javascript"]
1800
- }
1801
- ```
1802
-
1803
- > 임시저장 글을 발행할 때는 `status: "published"` 를 함께 전송한다.
1804
- > `tags` 배열을 포함하면 태그를 일괄 교체한다 (생략 시 기존 태그 유지).
1805
-
1806
- ### 6.5 게시글 삭제
1807
-
1808
- ```
1809
- DELETE /api/v1/board/posts/{seq}
1810
- ```
1811
-
1812
- > soft delete — `status: "deleted"` 처리.
1813
-
1814
- ### 6.6 댓글 목록
1815
-
1816
- ```
1817
- GET /api/v1/board/posts/{postSeq}/comments?page=1&per_page=50
1818
- ```
1819
-
1820
- > `postSeq`는 원글 또는 답글의 board_post seq 모두 가능.
1821
-
1822
- **Response:**
1823
-
1824
- ```json
1825
- {
1826
- "success": true,
1827
- "data": {
1828
- "items": [
1829
- {
1830
- "seq": 100,
1831
- "post_seq": 42,
1832
- "user_seq": 15,
1833
- "author_name": "박영희",
1834
- "content": "좋은 글이네요!",
1835
- "rating_sum": 8,
1836
- "rating_count": 2,
1837
- "created_time": "2026-03-06T11:00:00Z",
1838
- "updated_time": "2026-03-06T11:00:00Z"
1839
- }
1840
- ],
1841
- "pagination": {
1842
- "page": 1,
1843
- "per_page": 50,
1844
- "total": 5,
1845
- "total_pages": 1
1846
- }
1847
- }
1848
- }
1849
- ```
1850
-
1851
- > 익명 게시판(`anonymous_enabled = "Y"`) 댓글의 경우:
1852
- >
1853
- > - `author_name`은 `"익명"`으로 강제 대체되어 저장된다.
1854
- > - `user_seq`는 내부적으로 DB에 저장되지만, **응답 시 `null`로 마스킹**하여 반환한다 (본인 식별/신고 처리에만 내부 사용).
1855
-
1856
- ### 6.7 댓글 작성
1857
-
1858
- ```
1859
- POST /api/v1/board/posts/{postSeq}/comments/submit
1860
- Content-Type: application/json
1861
-
1862
- {
1863
- "content": "댓글 내용입니다."
1864
- }
1865
- ```
1866
-
1867
- > `postSeq`가 원글이면 원글에, 답글이면 해당 답글에 달린다.
1868
- > 앱서버에서 `post_seq = postSeq`, `root_seq = root_seq_of_post` 자동 설정.
1869
-
1870
- ### 6.8 파일 목록
1871
-
1872
- ```
1873
- GET /api/v1/board/posts/{postSeq}/files
1874
- ```
1875
-
1876
- **Response:**
1877
-
1878
- ```json
1879
- {
1880
- "success": true,
1881
- "data": {
1882
- "items": [
1883
- {
1884
- "uuid": "550e8400-e29b-41d4-a716-446655440000",
1885
- "original_name": "첨부자료.pdf",
1886
- "mime_type": "application/pdf",
1887
- "size": 204800,
1888
- "account_seq": 10,
1889
- "is_public": false,
1890
- "created_time": "2026-03-06T09:00:00Z"
1891
- }
1892
- ],
1893
- "total": 2
1894
- }
1895
- }
1896
- ```
1897
-
1898
- > 앱서버 핸들러에서 `entityServer.fileList("board_post", { refSeq: postSeq })` 호출.
1899
-
1900
- ### 6.9 파일 업로드
1901
-
1902
- ```
1903
- POST /api/v1/board/posts/{postSeq}/files
1904
- Content-Type: multipart/form-data
1905
-
1906
- file: (binary)
1907
- ```
1908
-
1909
- **Response:**
1910
-
1911
- ```json
1912
- {
1913
- "success": true,
1914
- "data": {
1915
- "uuid": "550e8400-e29b-41d4-a716-446655440000",
1916
- "original_name": "첨부자료.pdf",
1917
- "mime_type": "application/pdf",
1918
- "size": 204800,
1919
- "account_seq": 10,
1920
- "created_time": "2026-03-06T09:00:00Z"
1921
- }
1922
- }
1923
- ```
1924
-
1925
- > 앱서버 핸들러 처리:
1926
- >
1927
- > 1. `entityServer.fileUpload("board_post", file, { refSeq: postSeq })` 호출
1928
- > 2. 업로드 성공 후 `board_post.file_count` +1 갱신
1929
-
1930
- ### 6.10 파일 뷰 / 다운로드
1931
-
1932
- ```
1933
- # 브라우저 인라인 뷰 (이미지, PDF 등 바로보기)
1934
- GET /api/v1/board/files/{uuid}
1935
-
1936
- # 강제 다운로드
1937
- GET /api/v1/board/files/{uuid}?download
1938
- ```
1939
-
1940
- > 앱서버 핸들러에서 엔티티 서버 `GET /v1/files/{uuid}` 로 리다이렉트 또는 프록시.
1941
-
1942
- ### 6.11 파일 삭제
1943
-
1944
- ```
1945
- DELETE /api/v1/board/files/{uuid}
1946
- ```
1947
-
1948
- > 앱서버 핸들러 처리:
1949
- >
1950
- > 1. `entityServer.fileDelete("board_post", uuid)` 호출
1951
- > 2. 삭제 성공 후 `board_post.file_count` -1 갱신
1952
-
1953
- ### 6.12 비회원 글 작성
1954
-
1955
- ```
1956
- POST /api/v1/board/{category}/guest-submit
1957
- Content-Type: application/json
1958
-
1959
- {
1960
- "title": "문의드립니다",
1961
- "content": "<p>본문 내용</p>",
1962
- "guest_name": "홍길동",
1963
- "guest_password": "1234"
1964
- }
1965
- ```
1966
-
1967
- > 카테고리의 `guest_write_enabled = "Y"` 일 때만 허용.
1968
- > `guest_password`는 핸들러에서 bcrypt 해시 후 저장. 응답에는 비밀번호 미포함.
1969
-
1970
- **Response:**
1971
-
1972
- ```json
1973
- { "success": true, "data": { "seq": 55 } }
1974
- ```
1975
-
1976
- ### 6.13 비회원 본인 확인 (비밀번호 → 임시 토큰)
1977
-
1978
- ```
1979
- POST /api/v1/board/posts/{seq}/guest-auth
1980
- Content-Type: application/json
1981
-
1982
- { "guest_password": "1234" }
1983
- ```
1984
-
1985
- **Response:**
1986
-
1987
- ```json
1988
- {
1989
- "success": true,
1990
- "data": {
1991
- "token": "<jwt — expires 30m>",
1992
- "note": "이 토큰을 Authorization: Bearer 헤더에 담아 수정/삭제 요청"
1993
- }
1994
- }
1995
- ```
1996
-
1997
- > 수정·삭제 시 이 토큰을 Authorization 헤더에 담아 `posts.update` / `posts.remove` 에서 `req.user.guest_post_seq` 확인.
1998
-
1999
- ### 6.14 좋아요 토글
2000
-
2001
- ```
2002
- POST /api/v1/board/posts/{seq}/like
2003
- Authorization: Bearer <token>
2004
- ```
2005
-
2006
- **Response:**
2007
-
2008
- ```json
2009
- { "success": true, "data": { "liked": true } }
2010
- ```
2011
-
2012
- > 이미 좋아요된 상태면 취소(`liked: false`), 없으면 추가(`liked: true`).
2013
- > hook에 의해 `board_post.like_count` 자동 증감.
2014
-
2015
- ### 6.15 답변 채택
2016
-
2017
- ```
2018
- POST /api/v1/board/posts/{seq}/accept
2019
- Authorization: Bearer <token>
2020
- ```
2021
-
2022
- **Response:**
2023
-
2024
- ```json
2025
- { "success": true, "data": null }
2026
- ```
2027
-
2028
- > 원글 작성자 또는 관리자만 가능. 같은 `root_seq` 내 기존 채택글은 자동 해제.
2029
- > `board_post.accepted = "Y"` 로 설정.
2030
- > **원글(`parent_seq = null`)은 채택할 수 없다 — 답글(`depth ≥ 1`)만 채택 대상이다.**
2031
-
2032
- ### 6.16 별점 등록 / 수정
2033
-
2034
- 게시글·답글과 댓글 모두 별점을 줄 수 있다.
2035
-
2036
- **게시글·답글:**
2037
-
2038
- ```
2039
- POST /api/v1/board/posts/{seq}/rating
2040
- Authorization: Bearer <token>
2041
- Content-Type: application/json
2042
-
2043
- { "score": 4 }
2044
- ```
2045
-
2046
- **댓글:**
2047
-
2048
- ```
2049
- POST /api/v1/board/comments/{seq}/rating
2050
- Authorization: Bearer <token>
2051
- Content-Type: application/json
2052
-
2053
- { "score": 4 }
2054
- ```
2055
-
2056
- **Response (신규):**
2057
-
2058
- ```json
2059
- { "success": true, "data": { "created": true } }
2060
- ```
2061
-
2062
- **Response (수정):**
2063
-
2064
- ```json
2065
- { "success": true, "data": { "updated": true } }
2066
- ```
2067
-
2068
- > `score`는 1~5 정수. 동일 `target_type + target_seq + user_seq`로 재요청 시 수정 처리.
2069
- > hook에 의해 대상(`board_post` 또는 `board_comment`)의 `rating_sum`, `rating_count` 자동 갱신.
2070
- > 평균 별점: `rating_sum / rating_count` (프론트엔드에서 계산).
2071
-
2072
- ### 6.17 태그 목록 조회
2073
-
2074
- ```
2075
- GET /api/v1/board/tags?search={keyword}&limit=20
2076
- ```
2077
-
2078
- **Response:**
2079
-
2080
- ```json
2081
- {
2082
- "success": true,
2083
- "data": {
2084
- "items": [
2085
- {
2086
- "seq": 1,
2087
- "name": "javascript",
2088
- "label": "JavaScript",
2089
- "use_count": 42
2090
- },
2091
- { "seq": 2, "name": "vue-js", "label": "Vue.js", "use_count": 18 }
2092
- ]
2093
- }
2094
- }
2095
- ```
2096
-
2097
- > `use_count` 내림차순 정렬. `search`로 태그 이름/라벨 검색 가능. 입력 자동완성에 활용.
2098
-
2099
- ### 6.18 게시글 태그 일괄 설정
2100
-
2101
- ```
2102
- PUT /api/v1/board/posts/{seq}/tags
2103
- Authorization: Bearer <token>
2104
- Content-Type: application/json
2105
-
2106
- { "tags": ["javascript", "vue-js"] }
2107
- ```
2108
-
2109
- **Response:**
2110
-
2111
- ```json
2112
- { "success": true, "data": { "tags": ["javascript", "vue-js"] } }
2113
- ```
2114
-
2115
- > `tags`는 tag `name`(슬러그) 배열. 존재하지 않는 태그는 자동 생성 (label = name).
2116
- > 기존 `board_post_tag`를 **전체 삭제 후 재연결** (replace 방식).
2117
- > 빈 배열 `[]` 전송 시 태그 전체 해제.
2118
- > 게시글 작성(6.3)/수정(6.4) 시 `tags` 필드를 함께 보내면 이 로직을 내부 호출한다.
2119
-
2120
- ### 6.19 신고
2121
-
2122
- 게시글 또는 댓글을 신고한다. 한 회원이 동일 대상에 중복 신고할 수 없다.
2123
-
2124
- ```
2125
- POST /api/v1/board/posts/{seq}/report
2126
- POST /api/v1/board/comments/{seq}/report
2127
- Authorization: Bearer <token>
2128
- Content-Type: application/json
2129
-
2130
- {
2131
- "reason": "spam", // "spam" | "abuse" | "illegal" | "other"
2132
- "detail": "..." // 선택. reason이 "other"일 때 상세 내용
2133
- }
2134
- ```
2135
-
2136
- **Response:**
2137
-
2138
- ```json
2139
- { "success": true, "data": { "reported": true } }
2140
- ```
2141
-
2142
- **Error cases:**
2143
-
2144
- - `400` — 이미 신고한 대상 (`"이미 신고한 대상입니다."`)
2145
- - `404` — 존재하지 않는 게시글/댓글
2146
-
2147
- ### 6.19.1 신고 관리 (관리자)
2148
-
2149
- 신고 목록 조회 및 상태 변경. 관리자 전용.
2150
-
2151
- ```
2152
- GET /api/v1/board/admin/reports?status=pending&page=1&per_page=20
2153
- Authorization: Bearer <admin-token>
2154
- ```
2155
-
2156
- **Response:**
2157
-
2158
- ```json
2159
- {
2160
- "success": true,
2161
- "data": {
2162
- "items": [
2163
- {
2164
- "seq": 1,
2165
- "target_type": "post",
2166
- "target_seq": 42,
2167
- "user_seq": 15,
2168
- "reason": "spam",
2169
- "detail": null,
2170
- "status": "pending",
2171
- "created_time": "2026-03-06T12:00:00Z"
2172
- }
2173
- ],
2174
- "pagination": {
2175
- "page": 1,
2176
- "per_page": 20,
2177
- "total": 3,
2178
- "total_pages": 1
2179
- }
2180
- }
2181
- }
2182
- ```
2183
-
2184
- 신고 상태 변경:
2185
-
2186
- ```
2187
- PATCH /api/v1/board/admin/reports/{seq}
2188
- Authorization: Bearer <admin-token>
2189
- Content-Type: application/json
2190
-
2191
- { "status": "resolved" } // "resolved" | "dismissed"
2192
- ```
2193
-
2194
- ```json
2195
- { "success": true, "data": null }
2196
- ```
2197
-
2198
- > `status`를 `resolved`로 변경하면 해당 게시글/댓글을 숨김 처리(`status: "hidden"`) 할 수 있고,
2199
- > `dismissed`로 변경하면 신고를 기각한다.
2200
-
2201
- ### 6.20 읽음 표시
2202
-
2203
- 회원이 게시글을 읽음 처리한다. `board_read_log`에 upsert 방식으로 기록한다.
2204
- 상세 조회(6.2) 시 앱서버에서 자동 호출할 수 있고, 클라이언트가 명시적으로 호출할 수도 있다.
2205
-
2206
- ```
2207
- POST /api/v1/board/posts/{seq}/read
2208
- Authorization: Bearer <token>
2209
- ```
2210
-
2211
- **Response:**
2212
-
2213
- ```json
2214
- { "success": true, "data": { "read": true } }
2215
- ```
2216
-
2217
- > 비회원은 읽음 표시를 하지 않는다 (인증 필수).
2218
- > 목록 조회 시 `read` 여부는 `board_read_log` 조인으로 `is_read: true/false` 응답에 포함할 수 있다.
2219
- > **upsert 동시성 처리**: unique(`post_seq + user_seq`) 제약에 의해 `INSERT ON DUPLICATE KEY UPDATE read_time = NOW()` 또는
2220
- > Entity Server의 `submit`이 unique 충돌 시 자동 update하는 방식으로 race condition을 방지한다.
2221
-
2222
- ### 6.21 멘션 알림 목록
2223
-
2224
- 로그인한 회원의 읽지 않은 멘션 목록을 조회한다.
2225
-
2226
- ```
2227
- GET /api/v1/board/mentions?is_read=N
2228
- Authorization: Bearer <token>
2229
- ```
2230
-
2231
- **Response:**
2232
-
2233
- ```json
2234
- {
2235
- "success": true,
2236
- "data": {
2237
- "items": [
2238
- {
2239
- "seq": 1,
2240
- "source_type": "comment",
2241
- "source_seq": 42,
2242
- "source_title": "공지사항입니다",
2243
- "source_author_name": "홍길동",
2244
- "source_content_preview": "@당신의아이디 확인 부탁드립니다...",
2245
- "is_read": "N",
2246
- "created_time": "2025-01-01T00:00:00Z"
2247
- }
2248
- ],
2249
- "total": 1
2250
- }
2251
- }
2252
- ```
2253
-
2254
- > `source_title`은 멘션 소스가 속한 게시글 제목, `source_author_name`은 멘션을 작성한 사람,
2255
- > `source_content_preview`는 본문 앞부분 50자 미리보기. 프론트엔드 알림 UI 구성에 활용.
2256
-
2257
- 멘션 확인(읽음 처리):
2258
-
2259
- ```
2260
- PATCH /api/v1/board/mentions/{seq}/read
2261
- Authorization: Bearer <token>
2262
- ```
2263
-
2264
- ```json
2265
- { "success": true, "data": { "read": true } }
2266
- ```
2267
-
2268
- > 멘션 목록 조회/읽음 처리 모두 인증이 필요하며, 본인 멘션만 조회/처리 가능해야 한다.
2269
-
2270
- ## 7. 권한 및 보안
2271
-
2272
- ### 7.1 권한 매트릭스
2273
-
2274
- | 동작 | 비회원 | 일반 회원 | 관리자 |
2275
- | ---------------------------------------------- | ------------------------- | -------------------------------------- | ------ |
2276
- | 목록 조회 | read_role 에 따름 | ✅ | ✅ |
2277
- | 상세 조회 | read_role 에 따름 | ✅ | ✅ |
2278
- | 글 작성 (원글·답글, 회원) | ❌ | write_role 에 따름 | ✅ |
2279
- | 글 작성 (비회원 — `guest_write_enabled = "Y"`) | ✅ (비밀번호 필수) | — | ✅ |
2280
- | 글 수정 | ✅ (guest-auth 토큰 필요) | 본인만 | ✅ |
2281
- | 글 삭제 | ✅ (guest-auth 토큰 필요) | 본인만 | ✅ |
2282
- | 댓글 작성 | ❌ | ✅ | ✅ |
2283
- | 댓글 삭제 | ❌ | 본인만 | ✅ |
2284
- | 파일 업로드 | ❌ | write_role 에 따름 | ✅ |
2285
- | 파일 삭제 | ❌ | 본인(글 작성자)만 | ✅ |
2286
- | 좋아요 | ❌ | ✅ (`like_enabled = "Y"`) | ✅ |
2287
- | 별점 (게시글·답글) | ❌ | ✅ (`rating_enabled = "Y"`) | ✅ |
2288
- | 별점 (댓글) | ❌ | ✅ (`rating_enabled = "Y"`) | ✅ |
2289
- | 답변 채택 | ❌ | 원글 작성자만 (`accept_enabled = "Y"`) | ✅ |
2290
- | 글 고정(pin) | ❌ | ❌ | ✅ |
2291
- | 신고 | ❌ | ✅ (중복 신고 불가) | ✅ |
2292
- | 신고 관리 (목록·상태변경) | ❌ | ❌ | ✅ |
2293
- | 읽음 표시 | ❌ | ✅ (자동 또는 명시적) | ✅ |
2294
- | 카테고리 관리 | ❌ | ❌ | ✅ |
2295
-
2296
- ### 7.2 보안 고려사항
2297
-
2298
- - **XSS 방지**: content 필드는 앱서버 핸들러에서 HTML sanitize 처리
2299
- - **비회원 비밀번호**: `guest_password`는 bcrypt(cost=10) 해시 후 저장. 조회/수정/삭제 시 `bcrypt.compare` 검증. 평문을 응답·로그에 절대 포함하지 않는다.
2300
- - **비회원 임시 토큰**: `guest-auth` 응답 JWT는 `guest_post_seq` claim만 포함, 만료 30분. 해당 글 수정/삭제에만 사용 가능.
2301
- - **파일 업로드**: 엔티티 서버에서 MIME 타입 검증, SHA-256 중복 감지 처리. 앱서버에서 파일 크기 사전 제한
2302
- - **SQL Injection**: Entity Server의 parameterized query로 방어
2303
- - **Rate Limiting**: 앱서버 rate-limit 설정 (글 작성/댓글 작성/좋아요)
2304
- - **CSRF**: 앱서버 CSRF 플러그인 (`X-CSRF-Token` 헤더)
2305
- - **패킷 암호화**: 앱서버 packet-encrypt 플러그인 (선택)
2306
-
2307
- ---
2308
-
2309
- ## 8. 확장 고려사항
2310
-
2311
- 현재 구현된 확장 기능 (이미 설계에 포함):
2312
-
2313
- | 기능 | 위치 |
2314
- | ------------- | ----------------------------------------------------------------------------------------------------------- |
2315
- | 비회원 글작성 | 섹션 4.1 `guest_write_enabled`, 섹션 5.4 `guestCreate` 핸들러 |
2316
- | 좋아요/추천 | 섹션 4.5 `board_like`, 섹션 5.4 `likes.ts` 핸들러 |
2317
- | Push 알림 | 섹션 4.2 `board_post.after_insert` hook (`push_on_write = "Y"`) |
2318
- | 답변 채택 | 섹션 4.2 `board_post.accepted`, 섹션 5.4 `posts.accept` 핸들러 |
2319
- | 별점 | 섹션 4.6 `board_rating`, 섹션 5.4 `ratings.ts` 핸들러 |
2320
- | 임시저장 | `board_post.status = "draft"`, 6.1 `status=draft` 쿼리 파라미터, 6.3/6.4 `status` 필드 |
2321
- | 태그 | 섹션 4.7 `board_tag`, 4.8 `board_post_tag`, 6.17 목록, 6.18 일괄 설정, `tags.ts` 핸들러 |
2322
- | 신고 | 섹션 4.9 `board_report`, 6.19 신고 API, 6.19.1 관리자 신고 관리 API, `reports.ts` 핸들러 (게시글·댓글 공용) |
2323
- | 읽음 표시 | 섹션 4.10 `board_read_log`, 6.20 읽음 표시 API, `posts.markRead` 핸들러 |
2324
- | 멘션 | 섹션 4.11 `board_mention`, 6.21 멘션 목록 API, 댓글/글 작성 시 `@` 파싱 후 자동 삽입 |
2325
- | 익명 | 섹션 4.1 `anonymous_enabled`, 핸들러에서 `author_name` 강제·`user_seq` 마스킹 |
2326
-
2327
- 향후 필요 시 추가 가능한 기능들:
2328
-
2329
- | 기능 | 구현 방안 |
2330
- | ------------- | -------------------------------------------------------- |
2331
- | 에디터 이미지 | 앱서버에 이미지 업로드 라우트 추가 → content 내 URL 삽입 |
2332
-
2333
- ---
2334
-
2335
- ## 참고
2336
-
2337
- - Entity Server CRUD SDK: `entityServer.list()`, `entityServer.submit()` 등 (`@gateway/api`)
2338
- - 앱서버 라우트 패턴: `src/app/routes/board/route.ts` (Fastify auto-prefix `/api/v1/board/`)
2339
- - 기존 패턴 참고: `routes/account/`, `plugins/smtp/` 등의 핸들러 구조
2340
- - 엔티티 정의: `src/app/routes/entities/*.json` 에 배치 (board_category, board_post, board_comment, board_like, board_rating, board_tag, board_post_tag, board_report, board_read_log, board_mention)
2341
- - 파일 스토리지: 엔티티 서버 내장 files API (`/v1/files/board_post/*`), 별도 엔티티 불필요
2342
- - 파일 SDK: `entityServer.fileUpload()`, `entityServer.fileList()`, `entityServer.fileDelete()` (`entity-server-client`)