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.
- package/bin/create.js +47 -13
- package/dist/.env.example +68 -0
- package/dist/app/plugins/ocr/cache.ts +1 -0
- package/dist/app/plugins/ocr/config.ts +1 -0
- package/dist/app/plugins/ocr/direction.ts +1 -0
- package/dist/app/plugins/ocr/dispatch.ts +1 -0
- package/dist/app/plugins/ocr/entity-adapter.ts +1 -0
- package/dist/app/plugins/ocr/errors.ts +1 -0
- package/dist/app/plugins/ocr/handlers.ts +1 -0
- package/dist/app/plugins/ocr/index.ts +1 -0
- package/dist/app/plugins/ocr/llm-parser.ts +1 -0
- package/dist/app/plugins/ocr/parsing-pipeline.ts +1 -0
- package/dist/app/plugins/ocr/pdf-converter.ts +1 -0
- package/dist/app/plugins/ocr/preprocessor.ts +1 -0
- package/dist/app/plugins/ocr/providers/aws.ts +1 -0
- package/dist/app/plugins/ocr/providers/azure.ts +1 -0
- package/dist/app/plugins/ocr/providers/google.ts +1 -0
- package/dist/app/plugins/ocr/providers/index.ts +1 -0
- package/dist/app/plugins/ocr/providers/naver.ts +1 -0
- package/dist/app/plugins/ocr/providers/tesseract.ts +1 -0
- package/dist/app/plugins/ocr/providers/upstage.ts +1 -0
- package/dist/app/plugins/ocr/quota.ts +1 -0
- package/dist/app/plugins/ocr/refiner.ts +1 -0
- package/dist/app/plugins/ocr/routes.ts +1 -0
- package/dist/app/plugins/ocr/service.ts +1 -0
- package/dist/app/plugins/ocr/template-loader.ts +1 -0
- package/dist/app/plugins/ocr/template-matcher.ts +1 -0
- package/dist/app/plugins/ocr/types/config.ts +1 -0
- package/dist/app/plugins/ocr/types/driver.ts +1 -0
- package/dist/app/plugins/ocr/types/index.ts +1 -0
- package/dist/app/plugins/ocr/types/parsed.ts +1 -0
- package/dist/app/plugins/ocr/types/store.ts +1 -0
- package/dist/app/plugins/ocr/types/template.ts +1 -0
- package/dist/app/plugins/ocr/utils.ts +1 -0
- package/dist/app/plugins/sms/config.ts +1 -0
- package/dist/app/plugins/sms/entity-adapter.ts +1 -0
- package/dist/app/plugins/sms/handlers.ts +1 -0
- package/dist/app/plugins/sms/index.ts +1 -0
- package/dist/app/plugins/sms/providers/aligo.ts +1 -0
- package/dist/app/plugins/sms/providers/aws-sns.ts +1 -0
- package/dist/app/plugins/sms/providers/index.ts +1 -0
- package/dist/app/plugins/sms/providers/nhn.ts +1 -0
- package/dist/app/plugins/sms/providers/ppurio.ts +1 -0
- package/dist/app/plugins/sms/providers/solapi.ts +1 -0
- package/dist/app/plugins/sms/routes.ts +1 -0
- package/dist/app/plugins/sms/service.ts +1 -0
- package/dist/app/plugins/sms/types/client.ts +1 -0
- package/dist/app/plugins/sms/types/config.ts +1 -0
- package/dist/app/plugins/sms/types/index.ts +1 -0
- package/dist/app/plugins/sms/types/job.ts +1 -0
- package/dist/app/plugins/sms/verification.ts +1 -0
- package/dist/app/plugins/smtp/config.ts +1 -0
- package/dist/app/plugins/smtp/handlers.ts +1 -0
- package/dist/app/plugins/smtp/index.ts +1 -0
- package/dist/app/plugins/smtp/routes.ts +1 -0
- package/dist/app/plugins/smtp/types/config.ts +1 -0
- package/dist/app/plugins/smtp/types/index.ts +1 -0
- package/dist/logs/access.1.log +0 -0
- package/dist/system-api.js +1 -0
- package/dist/system.js +1 -0
- package/package.json +3 -8
- package/docs/design/board-api-design.md +0 -2342
- package/scripts/_gen-table-type.ts +0 -605
- package/scripts/build-minify-plugins.mjs +0 -124
- package/scripts/build-obfuscate-system.mjs +0 -38
- package/scripts/build.sh +0 -140
- package/scripts/gen-table-type.sh +0 -169
- package/scripts/push.sh +0 -102
- package/scripts/release.sh +0 -51
- package/src/app/plugins/2fa/config.json +0 -17
- package/src/app/plugins/ais/config.json +0 -7
- package/src/app/plugins/ais/config.ts +0 -32
- package/src/app/plugins/ais/docs/README.md +0 -142
- package/src/app/plugins/ais/docs/api.md +0 -138
- package/src/app/plugins/ais/entities/ais_vessel.json +0 -64
- package/src/app/plugins/ais/handlers.ts +0 -88
- package/src/app/plugins/ais/index.ts +0 -21
- package/src/app/plugins/ais/routes.ts +0 -13
- package/src/app/plugins/ais/service.ts +0 -242
- package/src/app/plugins/ais/types/index.ts +0 -78
- package/src/app/plugins/alimtalk/config.json +0 -26
- package/src/app/plugins/distance-server/config.json +0 -6
- package/src/app/plugins/distance-server/config.ts +0 -50
- package/src/app/plugins/distance-server/docs/README.md +0 -114
- package/src/app/plugins/distance-server/handlers.ts +0 -104
- package/src/app/plugins/distance-server/index.ts +0 -23
- package/src/app/plugins/distance-server/routes.ts +0 -36
- package/src/app/plugins/distance-server/service.ts +0 -187
- package/src/app/plugins/distance-server/types/index.ts +0 -8
- package/src/app/plugins/friendtalk/config.json +0 -11
- package/src/app/plugins/holidays/config.json +0 -10
- package/src/app/plugins/identity/config.json +0 -30
- package/src/app/plugins/kobc_freight/config.json +0 -6
- package/src/app/plugins/kobc_freight/config.ts +0 -28
- package/src/app/plugins/kobc_freight/docs/README.md +0 -316
- package/src/app/plugins/kobc_freight/entities/kobc_freight_entry.json +0 -31
- package/src/app/plugins/kobc_freight/entities/kobc_kcci_entry.json +0 -67
- package/src/app/plugins/kobc_freight/entities/kobc_kpli_entry.json +0 -27
- package/src/app/plugins/kobc_freight/entities/kobc_ncfi_entry.json +0 -99
- package/src/app/plugins/kobc_freight/handlers.ts +0 -283
- package/src/app/plugins/kobc_freight/index.ts +0 -21
- package/src/app/plugins/kobc_freight/routes.ts +0 -39
- package/src/app/plugins/kobc_freight/service.ts +0 -604
- package/src/app/plugins/kobc_freight/types/index.ts +0 -99
- package/src/app/plugins/llm/config.json +0 -71
- package/src/app/plugins/oauth/config.json +0 -72
- package/src/app/plugins/ocr/cache.ts +0 -50
- package/src/app/plugins/ocr/config.json +0 -110
- package/src/app/plugins/ocr/config.ts +0 -126
- package/src/app/plugins/ocr/direction.ts +0 -48
- package/src/app/plugins/ocr/dispatch.ts +0 -130
- package/src/app/plugins/ocr/entity-adapter.ts +0 -198
- package/src/app/plugins/ocr/errors.ts +0 -42
- package/src/app/plugins/ocr/handlers.ts +0 -250
- package/src/app/plugins/ocr/index.ts +0 -68
- package/src/app/plugins/ocr/llm-parser.ts +0 -164
- package/src/app/plugins/ocr/parsing-pipeline.ts +0 -87
- package/src/app/plugins/ocr/pdf-converter.ts +0 -136
- package/src/app/plugins/ocr/preprocessor.ts +0 -313
- package/src/app/plugins/ocr/providers/aws.ts +0 -200
- package/src/app/plugins/ocr/providers/azure.ts +0 -183
- package/src/app/plugins/ocr/providers/google.ts +0 -155
- package/src/app/plugins/ocr/providers/index.ts +0 -80
- package/src/app/plugins/ocr/providers/naver.ts +0 -186
- package/src/app/plugins/ocr/providers/tesseract.ts +0 -198
- package/src/app/plugins/ocr/providers/upstage.ts +0 -156
- package/src/app/plugins/ocr/quota.ts +0 -108
- package/src/app/plugins/ocr/refiner.ts +0 -112
- package/src/app/plugins/ocr/routes.ts +0 -19
- package/src/app/plugins/ocr/service.ts +0 -333
- package/src/app/plugins/ocr/template-loader.ts +0 -72
- package/src/app/plugins/ocr/template-matcher.ts +0 -422
- package/src/app/plugins/ocr/types/config.ts +0 -60
- package/src/app/plugins/ocr/types/driver.ts +0 -71
- package/src/app/plugins/ocr/types/index.ts +0 -5
- package/src/app/plugins/ocr/types/parsed.ts +0 -101
- package/src/app/plugins/ocr/types/store.ts +0 -70
- package/src/app/plugins/ocr/types/template.ts +0 -89
- package/src/app/plugins/ocr/utils.ts +0 -18
- package/src/app/plugins/pg/config.json +0 -35
- package/src/app/plugins/push/config.json +0 -18
- package/src/app/plugins/sms/config.json +0 -33
- package/src/app/plugins/sms/config.ts +0 -158
- package/src/app/plugins/sms/entity-adapter.ts +0 -213
- package/src/app/plugins/sms/handlers.ts +0 -149
- package/src/app/plugins/sms/index.ts +0 -93
- package/src/app/plugins/sms/providers/aligo.ts +0 -73
- package/src/app/plugins/sms/providers/aws-sns.ts +0 -182
- package/src/app/plugins/sms/providers/index.ts +0 -47
- package/src/app/plugins/sms/providers/nhn.ts +0 -82
- package/src/app/plugins/sms/providers/ppurio.ts +0 -76
- package/src/app/plugins/sms/providers/solapi.ts +0 -83
- package/src/app/plugins/sms/routes.ts +0 -23
- package/src/app/plugins/sms/service.ts +0 -239
- package/src/app/plugins/sms/types/client.ts +0 -41
- package/src/app/plugins/sms/types/config.ts +0 -46
- package/src/app/plugins/sms/types/index.ts +0 -3
- package/src/app/plugins/sms/types/job.ts +0 -51
- package/src/app/plugins/sms/verification.ts +0 -162
- package/src/app/plugins/smtp/config.ts +0 -41
- package/src/app/plugins/smtp/handlers.ts +0 -52
- package/src/app/plugins/smtp/index.ts +0 -33
- package/src/app/plugins/smtp/routes.ts +0 -19
- package/src/app/plugins/smtp/types/config.ts +0 -8
- package/src/app/plugins/smtp/types/index.ts +0 -1
- package/src/app/plugins/taxinvoice/config.json +0 -35
- package/src/app/routes/calendar/config.json +0 -5
- package/src/app/routes/calendar/entities/calendar_attendees.json +0 -23
- package/src/app/routes/calendar/entities/calendar_comments.json +0 -17
- package/src/app/routes/calendar/entities/calendar_events.json +0 -48
- package/src/app/routes/calendar/entities/calendar_kind.json +0 -11
- package/src/app/routes/calendar/entities/calendar_method.json +0 -11
- package/src/app/routes/calendar/routes.ts +0 -32
- package/src/app/routes/email-verify/config-loader.ts +0 -47
- package/src/app/routes/email-verify/config.example.json +0 -13
- package/src/app/routes/email-verify/config.json +0 -16
- package/src/app/routes/email-verify/entities/account.json +0 -23
- package/src/app/routes/email-verify/handlers/activate.ts +0 -103
- package/src/app/routes/email-verify/handlers/change.ts +0 -106
- package/src/app/routes/email-verify/handlers/confirm.ts +0 -87
- package/src/app/routes/email-verify/handlers/index.ts +0 -20
- package/src/app/routes/email-verify/handlers/send.ts +0 -157
- package/src/app/routes/email-verify/handlers/status.ts +0 -53
- package/src/app/routes/email-verify/handlers/utils.ts +0 -85
- package/src/app/routes/email-verify/routes.ts +0 -54
- package/src/app/routes/email-verify/templates/verification.html +0 -15
- package/src/app/routes/email-verify/templates/verification_link.html +0 -19
- package/src/app/routes/email-verify/types/index.ts +0 -77
- package/src/app/routes/email-verify/verification-utils.ts +0 -57
- package/src/app/routes/example-db/config.json +0 -5
- package/src/app/routes/example-db/handlers.ts +0 -220
- package/src/app/routes/example-db/models/account-ext.ts +0 -33
- package/src/app/routes/example-db/models/users.ts +0 -30
- package/src/app/routes/example-db/routes.ts +0 -23
- package/src/app/routes/example-db/types/defaults.ts +0 -21
- package/src/app/routes/example-db/types/index.ts +0 -4
- package/src/app/routes/example-db/types/params.ts +0 -3
- package/src/app/routes/example-db/types/query.ts +0 -6
- package/src/app/routes/example-db/types/user.ts +0 -11
- package/src/app/routes/example-es/config.json +0 -5
- package/src/app/routes/example-es/handlers.ts +0 -216
- package/src/app/routes/example-es/routes.ts +0 -24
- package/src/app/routes/example-es/types/defaults.ts +0 -30
- package/src/app/routes/example-es/types/index.ts +0 -4
- package/src/app/routes/example-es/types/params.ts +0 -3
- package/src/app/routes/example-es/types/post.ts +0 -12
- package/src/app/routes/example-es/types/query.ts +0 -14
- package/src/app/routes/funeral/config.json +0 -5
- package/src/app/routes/funeral/entities/funeral.json +0 -77
- package/src/app/routes/funeral/entities/funeral_docs.json +0 -36
- package/src/app/routes/funeral/entities/funeral_mourner.json +0 -31
- package/src/app/routes/funeral/entities/funeral_order.json +0 -48
- package/src/app/routes/funeral/entities/funeral_room.json +0 -61
- package/src/app/routes/funeral/entities/funeral_schedule.json +0 -39
- package/src/app/routes/funeral/routes.ts +0 -32
- package/src/app/routes/health/config.json +0 -5
- package/src/app/routes/health/handlers.ts +0 -69
- package/src/app/routes/health/routes.ts +0 -14
- package/src/app/routes/hr/career/config.json +0 -5
- package/src/app/routes/hr/career/entities/employee_career.json +0 -15
- package/src/app/routes/hr/career/routes.ts +0 -25
- package/src/app/routes/hr/config.json +0 -5
- package/src/app/routes/hr/education/config.json +0 -5
- package/src/app/routes/hr/education/entities/employee_education.json +0 -29
- package/src/app/routes/hr/education/entities/employee_education_mans.json +0 -25
- package/src/app/routes/hr/education/entities/employee_school.json +0 -19
- package/src/app/routes/hr/education/routes.ts +0 -28
- package/src/app/routes/hr/employee/config.json +0 -5
- package/src/app/routes/hr/employee/entities/employee.json +0 -59
- package/src/app/routes/hr/employee/entities/employee_cert.json +0 -19
- package/src/app/routes/hr/employee/entities/employee_reward.json +0 -21
- package/src/app/routes/hr/employee/routes.ts +0 -27
- package/src/app/routes/hr/entities/hr_group.json +0 -47
- package/src/app/routes/hr/entities/hr_group_pv.json +0 -20
- package/src/app/routes/hr/entities/hr_role.json +0 -43
- package/src/app/routes/hr/entities/hr_role_pv.json +0 -20
- package/src/app/routes/hr/routes.ts +0 -29
- package/src/app/routes/messages/chat/config.json +0 -5
- package/src/app/routes/messages/chat/entities/user_chat.json +0 -47
- package/src/app/routes/messages/chat/entities/user_chat_room.json +0 -38
- package/src/app/routes/messages/chat/entities/user_chat_room_member.json +0 -49
- package/src/app/routes/messages/chat/routes.ts +0 -28
- package/src/app/routes/messages/msgbox/config.json +0 -5
- package/src/app/routes/messages/msgbox/entities/user_msgbox.json +0 -73
- package/src/app/routes/messages/msgbox/routes.ts +0 -28
- package/src/app/routes/vessel-tracking/config.json +0 -3
- package/src/app/routes/vessel-tracking/entities/tracked_vessel.json +0 -261
- package/src/app/routes/vessel-tracking/handlers.ts +0 -134
- package/src/app/routes/vessel-tracking/routes.ts +0 -25
- package/src/app/routes/vessel-tracking/types/index.ts +0 -5
- package/src/app/routes/vessel-tracking/types/vessel.ts +0 -59
- package/src/app/schedules/ais_sync/config.json +0 -4
- package/src/app/schedules/ais_sync/index.ts +0 -69
- package/src/app/schedules/kobc_freight_sync/config.json +0 -4
- package/src/app/schedules/kobc_freight_sync/index.ts +0 -94
- package/src/app/schedules/vessel_kr_sync/config.json +0 -4
- package/src/app/schedules/vessel_kr_sync/index.ts +0 -72
- package/src/system/app.ts +0 -129
- package/src/system/cache/_store-ref.ts +0 -15
- package/src/system/cache/config.ts +0 -61
- package/src/system/cache/drivers/memcached.ts +0 -135
- package/src/system/cache/drivers/memory.ts +0 -92
- package/src/system/cache/drivers/redis.ts +0 -109
- package/src/system/cache/index.ts +0 -43
- package/src/system/cache/namespaced.ts +0 -79
- package/src/system/cache/plugin.ts +0 -59
- package/src/system/cache/types.ts +0 -81
- package/src/system/config/config-path.ts +0 -20
- package/src/system/config/cors.ts +0 -49
- package/src/system/config/database.ts +0 -190
- package/src/system/config/entity-server.ts +0 -8
- package/src/system/config/env-substitution.ts +0 -4
- package/src/system/config/env.ts +0 -30
- package/src/system/config/json-config.ts +0 -13
- package/src/system/config/module-path.ts +0 -16
- package/src/system/config/packet-encrypt.ts +0 -80
- package/src/system/config/rate-limit.ts +0 -4
- package/src/system/config/security-loader.ts +0 -25
- package/src/system/config/security.ts +0 -16
- package/src/system/config/server.ts +0 -81
- package/src/system/crypto/cipher.ts +0 -117
- package/src/system/crypto/data-encrypt.ts +0 -174
- package/src/system/crypto/hash.ts +0 -24
- package/src/system/crypto/packet.test.ts +0 -23
- package/src/system/crypto/packet.ts +0 -97
- package/src/system/crypto/random.ts +0 -19
- package/src/system/email/sender.ts +0 -85
- package/src/system/email/template-engine.ts +0 -147
- package/src/system/entity-server/bootstrap.ts +0 -270
- package/src/system/entity-server/client.ts +0 -64
- package/src/system/hooks/loader.ts +0 -32
- package/src/system/hooks/runner.ts +0 -159
- package/src/system/hooks/types.ts +0 -75
- package/src/system/hooks/withdraw-hooks.ts +0 -42
- package/src/system/http/cookie.ts +0 -62
- package/src/system/http/response.ts +0 -16
- package/src/system/index.ts +0 -48
- package/src/system/logging/log-format.ts +0 -50
- package/src/system/logging/logger.ts +0 -104
- package/src/system/middleware/_db-ref.ts +0 -26
- package/src/system/middleware/_push-ref.ts +0 -28
- package/src/system/middleware/access-log.ts +0 -34
- package/src/system/middleware/auth.ts +0 -67
- package/src/system/middleware/csrf.ts +0 -172
- package/src/system/middleware/database.ts +0 -44
- package/src/system/middleware/error-handler.ts +0 -51
- package/src/system/middleware/extension-loader.ts +0 -111
- package/src/system/middleware/packet-encrypt.ts +0 -281
- package/src/system/middleware/request-id.ts +0 -18
- package/src/system/plugins/access-log.ts +0 -34
- package/src/system/plugins/packet-encrypt.ts +0 -281
- package/src/system/proxy/register.ts +0 -37
- package/src/system/public-api.ts +0 -140
- package/src/system/push/sender.ts +0 -131
- package/src/system/routes/entity-interceptor.ts +0 -327
- package/src/system/routes/loader.ts +0 -215
- package/src/system/scheduler/cron-utils.ts +0 -150
- package/src/system/scheduler/distributed-lock.ts +0 -141
- package/src/system/scheduler/schedule-loader.ts +0 -105
- package/src/system/security/anonymous-device-id.ts +0 -41
- package/src/system/security/anonymous-device.ts +0 -98
- package/src/system/security/anonymous-packet-token.ts +0 -23
- package/src/system/security/packet-bootstrap.ts +0 -16
- package/src/system/security/password-policy.ts +0 -191
- package/src/system/startup-banner.ts +0 -191
- package/src/system/types/fastify.d.ts +0 -53
- package/src/system/utils/app-path.ts +0 -31
- package/src/system/utils/coerce.ts +0 -28
- package/src/system/utils/date-prefixed-log-stream.ts +0 -176
- package/src/system/utils/errors.ts +0 -66
- package/src/system/utils/format.ts +0 -45
- package/src/system/utils/http-client.ts +0 -79
- package/src/system/utils/user-agent.ts +0 -82
- package/tsconfig.app.json +0 -17
- package/tsconfig.json +0 -39
- /package/{.env.example → dist/.env} +0 -0
- /package/{src → dist}/app/hooks/README.md +0 -0
- /package/{src → dist}/app/hooks/account.ts +0 -0
- /package/{src → dist}/app/hooks/index.ts +0 -0
- /package/{src → dist}/app/hooks/order.ts +0 -0
- /package/{src → dist}/app/hooks/post.ts +0 -0
- /package/{src/app/plugins/2fa/config.example.json → dist/app/plugins/2fa/config.json} +0 -0
- /package/{src → dist}/app/plugins/2fa/config.ts +0 -0
- /package/{src → dist}/app/plugins/2fa/docs/README.md +0 -0
- /package/{src → dist}/app/plugins/2fa/entities/account.json +0 -0
- /package/{src → dist}/app/plugins/2fa/handlers/disable.ts +0 -0
- /package/{src → dist}/app/plugins/2fa/handlers/index.ts +0 -0
- /package/{src → dist}/app/plugins/2fa/handlers/recovery.ts +0 -0
- /package/{src → dist}/app/plugins/2fa/handlers/regenerate.ts +0 -0
- /package/{src → dist}/app/plugins/2fa/handlers/setup-verify.ts +0 -0
- /package/{src → dist}/app/plugins/2fa/handlers/setup.ts +0 -0
- /package/{src → dist}/app/plugins/2fa/handlers/status.ts +0 -0
- /package/{src → dist}/app/plugins/2fa/handlers/utils.ts +0 -0
- /package/{src → dist}/app/plugins/2fa/handlers/verify.ts +0 -0
- /package/{src → dist}/app/plugins/2fa/index.ts +0 -0
- /package/{src → dist}/app/plugins/2fa/routes.ts +0 -0
- /package/{src → dist}/app/plugins/2fa/templates/auth/2fa_disabled.html +0 -0
- /package/{src → dist}/app/plugins/2fa/templates/auth/2fa_recovery_regenerated.html +0 -0
- /package/{src → dist}/app/plugins/2fa/templates/auth/2fa_setup_complete.html +0 -0
- /package/{src → dist}/app/plugins/2fa/totp-utils.ts +0 -0
- /package/{src → dist}/app/plugins/2fa/types.ts +0 -0
- /package/{src → dist}/app/plugins/README.md +0 -0
- /package/{src/app/plugins/alimtalk/config.example.json → dist/app/plugins/alimtalk/config.json} +0 -0
- /package/{src → dist}/app/plugins/alimtalk/config.ts +0 -0
- /package/{src → dist}/app/plugins/alimtalk/docs/README.md +0 -0
- /package/{src → dist}/app/plugins/alimtalk/entities/alimtalk_log.json +0 -0
- /package/{src → dist}/app/plugins/alimtalk/entities/alimtalk_msg.json +0 -0
- /package/{src → dist}/app/plugins/alimtalk/entity-adapter.ts +0 -0
- /package/{src → dist}/app/plugins/alimtalk/handlers.ts +0 -0
- /package/{src → dist}/app/plugins/alimtalk/index.ts +0 -0
- /package/{src → dist}/app/plugins/alimtalk/providers/aligo.ts +0 -0
- /package/{src → dist}/app/plugins/alimtalk/providers/index.ts +0 -0
- /package/{src → dist}/app/plugins/alimtalk/providers/nhn.ts +0 -0
- /package/{src → dist}/app/plugins/alimtalk/providers/ppurio.ts +0 -0
- /package/{src → dist}/app/plugins/alimtalk/providers/solapi.ts +0 -0
- /package/{src → dist}/app/plugins/alimtalk/routes.ts +0 -0
- /package/{src → dist}/app/plugins/alimtalk/service.ts +0 -0
- /package/{src → dist}/app/plugins/alimtalk/template-cache.ts +0 -0
- /package/{src → dist}/app/plugins/alimtalk/templates/alimtalk.json +0 -0
- /package/{src → dist}/app/plugins/alimtalk/types/client.ts +0 -0
- /package/{src → dist}/app/plugins/alimtalk/types/config.ts +0 -0
- /package/{src → dist}/app/plugins/alimtalk/types/friendtalk.ts +0 -0
- /package/{src → dist}/app/plugins/alimtalk/types/index.ts +0 -0
- /package/{src → dist}/app/plugins/alimtalk/types/job.ts +0 -0
- /package/{src → dist}/app/plugins/alimtalk/webhook.ts +0 -0
- /package/{src → dist}/app/plugins/example/config.json +0 -0
- /package/{src → dist}/app/plugins/example/config.ts +0 -0
- /package/{src → dist}/app/plugins/example/docs/README.md +0 -0
- /package/{src → dist}/app/plugins/example/entity-adapter.ts +0 -0
- /package/{src → dist}/app/plugins/example/handlers.ts +0 -0
- /package/{src → dist}/app/plugins/example/index.ts +0 -0
- /package/{src → dist}/app/plugins/example/routes.ts +0 -0
- /package/{src → dist}/app/plugins/example/service.ts +0 -0
- /package/{src → dist}/app/plugins/example/types/config.ts +0 -0
- /package/{src → dist}/app/plugins/example/types/index.ts +0 -0
- /package/{src/app/plugins/friendtalk/config.example.json → dist/app/plugins/friendtalk/config.json} +0 -0
- /package/{src → dist}/app/plugins/friendtalk/config.ts +0 -0
- /package/{src → dist}/app/plugins/friendtalk/docs/README.md +0 -0
- /package/{src → dist}/app/plugins/friendtalk/entities/friendtalk_log.json +0 -0
- /package/{src → dist}/app/plugins/friendtalk/entities/friendtalk_msg.json +0 -0
- /package/{src → dist}/app/plugins/friendtalk/entity-adapter.ts +0 -0
- /package/{src → dist}/app/plugins/friendtalk/handlers.ts +0 -0
- /package/{src → dist}/app/plugins/friendtalk/routes.ts +0 -0
- /package/{src → dist}/app/plugins/friendtalk/templates/friendtalk.json +0 -0
- /package/{src/app/plugins/holidays/config.example.json → dist/app/plugins/holidays/config.json} +0 -0
- /package/{src → dist}/app/plugins/holidays/config.ts +0 -0
- /package/{src → dist}/app/plugins/holidays/docs/README.md +0 -0
- /package/{src → dist}/app/plugins/holidays/entities/holiday.json +0 -0
- /package/{src → dist}/app/plugins/holidays/handlers.ts +0 -0
- /package/{src → dist}/app/plugins/holidays/index.ts +0 -0
- /package/{src → dist}/app/plugins/holidays/routes.ts +0 -0
- /package/{src → dist}/app/plugins/holidays/service.ts +0 -0
- /package/{src → dist}/app/plugins/holidays/types/api.ts +0 -0
- /package/{src → dist}/app/plugins/holidays/types/config.ts +0 -0
- /package/{src → dist}/app/plugins/holidays/types/index.ts +0 -0
- /package/{src/app/plugins/identity/config.example.json → dist/app/plugins/identity/config.json} +0 -0
- /package/{src → dist}/app/plugins/identity/config.ts +0 -0
- /package/{src → dist}/app/plugins/identity/crypto.ts +0 -0
- /package/{src → dist}/app/plugins/identity/docs/README.md +0 -0
- /package/{src → dist}/app/plugins/identity/entities/account.json +0 -0
- /package/{src → dist}/app/plugins/identity/entities/identity_verification.json +0 -0
- /package/{src → dist}/app/plugins/identity/entity-adapter.ts +0 -0
- /package/{src → dist}/app/plugins/identity/handlers.ts +0 -0
- /package/{src → dist}/app/plugins/identity/index.ts +0 -0
- /package/{src → dist}/app/plugins/identity/providers/danal.ts +0 -0
- /package/{src → dist}/app/plugins/identity/providers/index.ts +0 -0
- /package/{src → dist}/app/plugins/identity/providers/kmc.ts +0 -0
- /package/{src → dist}/app/plugins/identity/providers/nice.ts +0 -0
- /package/{src → dist}/app/plugins/identity/routes.ts +0 -0
- /package/{src → dist}/app/plugins/identity/service.ts +0 -0
- /package/{src → dist}/app/plugins/identity/types/config.ts +0 -0
- /package/{src → dist}/app/plugins/identity/types/index.ts +0 -0
- /package/{src → dist}/app/plugins/identity/types/verification.ts +0 -0
- /package/{src → dist}/app/plugins/llm/cache.ts +0 -0
- /package/{src → dist}/app/plugins/llm/chatbot-store.ts +0 -0
- /package/{src → dist}/app/plugins/llm/chunker.ts +0 -0
- /package/{src/app/plugins/llm/config.example.json → dist/app/plugins/llm/config.json} +0 -0
- /package/{src → dist}/app/plugins/llm/config.ts +0 -0
- /package/{src → dist}/app/plugins/llm/conversation-store.ts +0 -0
- /package/{src → dist}/app/plugins/llm/docs/README.md +0 -0
- /package/{src → dist}/app/plugins/llm/docs/api.md +0 -0
- /package/{src → dist}/app/plugins/llm/document-store.ts +0 -0
- /package/{src → dist}/app/plugins/llm/entities/llm_chatbot.json +0 -0
- /package/{src → dist}/app/plugins/llm/entities/llm_conversation.json +0 -0
- /package/{src → dist}/app/plugins/llm/entities/llm_document.json +0 -0
- /package/{src → dist}/app/plugins/llm/entities/llm_usage.json +0 -0
- /package/{src → dist}/app/plugins/llm/entities/llm_user_profile.json +0 -0
- /package/{src → dist}/app/plugins/llm/handlers.ts +0 -0
- /package/{src → dist}/app/plugins/llm/index.ts +0 -0
- /package/{src → dist}/app/plugins/llm/profile-store.ts +0 -0
- /package/{src → dist}/app/plugins/llm/providers/anthropic.ts +0 -0
- /package/{src → dist}/app/plugins/llm/providers/azure.ts +0 -0
- /package/{src → dist}/app/plugins/llm/providers/gemini.ts +0 -0
- /package/{src → dist}/app/plugins/llm/providers/index.ts +0 -0
- /package/{src → dist}/app/plugins/llm/providers/ollama.ts +0 -0
- /package/{src → dist}/app/plugins/llm/providers/openai.ts +0 -0
- /package/{src → dist}/app/plugins/llm/routes.ts +0 -0
- /package/{src → dist}/app/plugins/llm/service.ts +0 -0
- /package/{src → dist}/app/plugins/llm/template-loader.ts +0 -0
- /package/{src → dist}/app/plugins/llm/templates/prompts/extract_json.json +0 -0
- /package/{src → dist}/app/plugins/llm/templates/prompts/summarize.json +0 -0
- /package/{src → dist}/app/plugins/llm/templates/prompts/translate.json +0 -0
- /package/{src → dist}/app/plugins/llm/types/chat.ts +0 -0
- /package/{src → dist}/app/plugins/llm/types/chatbot.ts +0 -0
- /package/{src → dist}/app/plugins/llm/types/config.ts +0 -0
- /package/{src → dist}/app/plugins/llm/types/conversation.ts +0 -0
- /package/{src → dist}/app/plugins/llm/types/index.ts +0 -0
- /package/{src → dist}/app/plugins/llm/types/profile.ts +0 -0
- /package/{src → dist}/app/plugins/llm/types/store.ts +0 -0
- /package/{src → dist}/app/plugins/llm/types/usage.ts +0 -0
- /package/{src → dist}/app/plugins/llm/usage-store.ts +0 -0
- /package/{src → dist}/app/plugins/oauth/account/handlers/index.ts +0 -0
- /package/{src → dist}/app/plugins/oauth/account/handlers/link.ts +0 -0
- /package/{src → dist}/app/plugins/oauth/account/handlers/providers-list.ts +0 -0
- /package/{src → dist}/app/plugins/oauth/account/handlers/refresh.ts +0 -0
- /package/{src → dist}/app/plugins/oauth/account/handlers/unlink.ts +0 -0
- /package/{src/app/plugins/oauth/config.example.json → dist/app/plugins/oauth/config.json} +0 -0
- /package/{src → dist}/app/plugins/oauth/config.ts +0 -0
- /package/{src → dist}/app/plugins/oauth/docs/README.md +0 -0
- /package/{src → dist}/app/plugins/oauth/entities/account_oauth.json +0 -0
- /package/{src → dist}/app/plugins/oauth/handlers/callback.ts +0 -0
- /package/{src → dist}/app/plugins/oauth/handlers/index.ts +0 -0
- /package/{src → dist}/app/plugins/oauth/handlers/redirect.ts +0 -0
- /package/{src → dist}/app/plugins/oauth/index.ts +0 -0
- /package/{src → dist}/app/plugins/oauth/providers/index.ts +0 -0
- /package/{src → dist}/app/plugins/oauth/routes.ts +0 -0
- /package/{src → dist}/app/plugins/oauth/service.ts +0 -0
- /package/{src → dist}/app/plugins/oauth/state.ts +0 -0
- /package/{src → dist}/app/plugins/oauth/types/index.ts +0 -0
- /package/{src → dist}/app/plugins/oauth/upsert.ts +0 -0
- /package/{src/app/plugins/ocr/config.example.json → dist/app/plugins/ocr/config.json} +0 -0
- /package/{src → dist}/app/plugins/ocr/docs/README.md +0 -0
- /package/{src → dist}/app/plugins/ocr/docs/api.md +0 -0
- /package/{src → dist}/app/plugins/ocr/entities/ocr_result.json +0 -0
- /package/{src → dist}/app/plugins/ocr/entities/ocr_usage.json +0 -0
- /package/{src → dist}/app/plugins/ocr/templates/business_reg.json +0 -0
- /package/{src → dist}/app/plugins/ocr/templates/career_cert.json +0 -0
- /package/{src → dist}/app/plugins/ocr/templates/driver_license.json +0 -0
- /package/{src → dist}/app/plugins/ocr/templates/facility_card.json +0 -0
- /package/{src → dist}/app/plugins/ocr/templates/id_card.json +0 -0
- /package/{src → dist}/app/plugins/ocr/templates/invoice.json +0 -0
- /package/{src → dist}/app/plugins/ocr/templates/namecard.json +0 -0
- /package/{src → dist}/app/plugins/ocr/templates/prompts/business_reg.json +0 -0
- /package/{src → dist}/app/plugins/ocr/templates/prompts/career_cert.json +0 -0
- /package/{src → dist}/app/plugins/ocr/templates/prompts/driver_license.json +0 -0
- /package/{src → dist}/app/plugins/ocr/templates/prompts/facility_card.json +0 -0
- /package/{src → dist}/app/plugins/ocr/templates/prompts/general.json +0 -0
- /package/{src → dist}/app/plugins/ocr/templates/prompts/id_card.json +0 -0
- /package/{src → dist}/app/plugins/ocr/templates/prompts/invoice.json +0 -0
- /package/{src → dist}/app/plugins/ocr/templates/prompts/namecard.json +0 -0
- /package/{src → dist}/app/plugins/ocr/templates/prompts/receipt.json +0 -0
- /package/{src → dist}/app/plugins/ocr/templates/receipt.json +0 -0
- /package/{src/app/plugins/pg/config.example.json → dist/app/plugins/pg/config.json} +0 -0
- /package/{src → dist}/app/plugins/pg/config.ts +0 -0
- /package/{src → dist}/app/plugins/pg/docs/README.md +0 -0
- /package/{src → dist}/app/plugins/pg/entities/pg_cancel.json +0 -0
- /package/{src → dist}/app/plugins/pg/entities/pg_order.json +0 -0
- /package/{src → dist}/app/plugins/pg/entities/pg_webhook_log.json +0 -0
- /package/{src → dist}/app/plugins/pg/entity-adapter.ts +0 -0
- /package/{src → dist}/app/plugins/pg/handlers.ts +0 -0
- /package/{src → dist}/app/plugins/pg/index.ts +0 -0
- /package/{src → dist}/app/plugins/pg/providers/danal.ts +0 -0
- /package/{src → dist}/app/plugins/pg/providers/hecto.ts +0 -0
- /package/{src → dist}/app/plugins/pg/providers/index.ts +0 -0
- /package/{src → dist}/app/plugins/pg/providers/inicis.ts +0 -0
- /package/{src → dist}/app/plugins/pg/providers/kakaopay.ts +0 -0
- /package/{src → dist}/app/plugins/pg/providers/kcp.ts +0 -0
- /package/{src → dist}/app/plugins/pg/providers/naverpay.ts +0 -0
- /package/{src → dist}/app/plugins/pg/providers/payco.ts +0 -0
- /package/{src → dist}/app/plugins/pg/providers/payletter.ts +0 -0
- /package/{src → dist}/app/plugins/pg/providers/paypal.ts +0 -0
- /package/{src → dist}/app/plugins/pg/providers/toss.ts +0 -0
- /package/{src → dist}/app/plugins/pg/providers/wanna.ts +0 -0
- /package/{src → dist}/app/plugins/pg/routes.ts +0 -0
- /package/{src → dist}/app/plugins/pg/service.ts +0 -0
- /package/{src → dist}/app/plugins/pg/types/client.ts +0 -0
- /package/{src → dist}/app/plugins/pg/types/config.ts +0 -0
- /package/{src → dist}/app/plugins/pg/types/error.ts +0 -0
- /package/{src → dist}/app/plugins/pg/types/index.ts +0 -0
- /package/{src → dist}/app/plugins/pg/types/payment.ts +0 -0
- /package/{src → dist}/app/plugins/providers/docs/README.md +0 -0
- /package/{src → dist}/app/plugins/providers/solapi-auth.ts +0 -0
- /package/{src/app/plugins/push/config.example.json → dist/app/plugins/push/config.json} +0 -0
- /package/{src → dist}/app/plugins/push/config.ts +0 -0
- /package/{src → dist}/app/plugins/push/docs/README.md +0 -0
- /package/{src → dist}/app/plugins/push/entities/push_log.json +0 -0
- /package/{src → dist}/app/plugins/push/entities/push_msg.json +0 -0
- /package/{src → dist}/app/plugins/push/entity-adapter.ts +0 -0
- /package/{src → dist}/app/plugins/push/handlers.ts +0 -0
- /package/{src → dist}/app/plugins/push/index.ts +0 -0
- /package/{src → dist}/app/plugins/push/providers/apns.ts +0 -0
- /package/{src → dist}/app/plugins/push/providers/fcm.ts +0 -0
- /package/{src → dist}/app/plugins/push/providers/index.ts +0 -0
- /package/{src → dist}/app/plugins/push/providers/utils.ts +0 -0
- /package/{src → dist}/app/plugins/push/routes.ts +0 -0
- /package/{src → dist}/app/plugins/push/service.ts +0 -0
- /package/{src → dist}/app/plugins/push/types/config.ts +0 -0
- /package/{src → dist}/app/plugins/push/types/index.ts +0 -0
- /package/{src → dist}/app/plugins/push/types/job.ts +0 -0
- /package/{src → dist}/app/plugins/shared/docs/README.md +0 -0
- /package/{src/app/plugins/sms/config.example.json → dist/app/plugins/sms/config.json} +0 -0
- /package/{src → dist}/app/plugins/sms/docs/README.md +0 -0
- /package/{src → dist}/app/plugins/sms/entities/sms_log.json +0 -0
- /package/{src → dist}/app/plugins/sms/entities/sms_msg.json +0 -0
- /package/{src → dist}/app/plugins/sms/entities/sms_verification.json +0 -0
- /package/{src → dist}/app/plugins/smtp/config.json +0 -0
- /package/{src → dist}/app/plugins/smtp/docs/README.md +0 -0
- /package/{src → dist}/app/plugins/smtp/templates/layout.html +0 -0
- /package/{src/app/plugins/taxinvoice/config.example.json → dist/app/plugins/taxinvoice/config.json} +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/config.ts +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/docs/README.md +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/entities/tax_invoice.json +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/entities/tax_invoice_item.json +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/entities/tax_invoice_log.json +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/entities/tax_invoice_party.json +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/entity-adapter.ts +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/handlers.ts +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/index.ts +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/providers/barobill.ts +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/providers/bolta.ts +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/providers/esero.ts +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/providers/index.ts +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/providers/popbill.ts +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/providers/sendbill.ts +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/providers/smartbill.ts +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/routes.ts +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/service.ts +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/types/client.ts +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/types/config.ts +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/types/index.ts +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/types/invoice.ts +0 -0
- /package/{src → dist}/app/plugins/taxinvoice/types/queue.ts +0 -0
- /package/{src → dist}/app/plugins/vessel_kr/config.json +0 -0
- /package/{src → dist}/app/plugins/vessel_kr/config.ts +0 -0
- /package/{src → dist}/app/plugins/vessel_kr/docs/README.md +0 -0
- /package/{src → dist}/app/plugins/vessel_kr/entities/vessel_kr_entry.json +0 -0
- /package/{src → dist}/app/plugins/vessel_kr/handlers.ts +0 -0
- /package/{src → dist}/app/plugins/vessel_kr/index.ts +0 -0
- /package/{src → dist}/app/plugins/vessel_kr/routes.ts +0 -0
- /package/{src → dist}/app/plugins/vessel_kr/service.ts +0 -0
- /package/{src → dist}/app/plugins/vessel_kr/types/index.ts +0 -0
- /package/{src → dist}/app/routes/README.md +0 -0
- /package/{src → dist}/app/routes/account/change-password/config.json +0 -0
- /package/{src → dist}/app/routes/account/change-password/entities/password_history.json +0 -0
- /package/{src → dist}/app/routes/account/change-password/handlers.ts +0 -0
- /package/{src → dist}/app/routes/account/change-password/routes.ts +0 -0
- /package/{src → dist}/app/routes/account/config.json +0 -0
- /package/{src → dist}/app/routes/account/reactivate/config.json +0 -0
- /package/{src → dist}/app/routes/account/reactivate/handlers.ts +0 -0
- /package/{src → dist}/app/routes/account/reactivate/routes.ts +0 -0
- /package/{src → dist}/app/routes/account/register/config-loader.ts +0 -0
- /package/{src → dist}/app/routes/account/register/config.json +0 -0
- /package/{src → dist}/app/routes/account/register/handlers.ts +0 -0
- /package/{src → dist}/app/routes/account/register/routes.ts +0 -0
- /package/{src → dist}/app/routes/account/register/types/index.ts +0 -0
- /package/{src → dist}/app/routes/account/routes.ts +0 -0
- /package/{src → dist}/app/routes/account/templates/force_reset.html +0 -0
- /package/{src → dist}/app/routes/account/templates/welcome.html +0 -0
- /package/{src → dist}/app/routes/account/withdraw/handlers.ts +0 -0
- /package/{src → dist}/app/routes/account/withdraw/routes.ts +0 -0
- /package/{src → dist}/app/routes/approval/config.json +0 -0
- /package/{src → dist}/app/routes/approval/entities/approval.json +0 -0
- /package/{src → dist}/app/routes/approval/entities/comments.json +0 -0
- /package/{src → dist}/app/routes/approval/entities/reference.json +0 -0
- /package/{src → dist}/app/routes/approval/routes.ts +0 -0
- /package/{src → dist}/app/routes/auth/config.json +0 -0
- /package/{src → dist}/app/routes/auth/handlers.ts +0 -0
- /package/{src → dist}/app/routes/auth/routes.ts +0 -0
- /package/{src → dist}/app/routes/board/config.json +0 -0
- /package/{src → dist}/app/routes/board/entities/board_category.json +0 -0
- /package/{src → dist}/app/routes/board/entities/board_comment.json +0 -0
- /package/{src → dist}/app/routes/board/entities/board_like.json +0 -0
- /package/{src → dist}/app/routes/board/entities/board_mention.json +0 -0
- /package/{src → dist}/app/routes/board/entities/board_post.json +0 -0
- /package/{src → dist}/app/routes/board/entities/board_post_tag.json +0 -0
- /package/{src → dist}/app/routes/board/entities/board_rating.json +0 -0
- /package/{src → dist}/app/routes/board/entities/board_read_log.json +0 -0
- /package/{src → dist}/app/routes/board/entities/board_report.json +0 -0
- /package/{src → dist}/app/routes/board/entities/board_tag.json +0 -0
- /package/{src → dist}/app/routes/board/handlers/categories.ts +0 -0
- /package/{src → dist}/app/routes/board/handlers/comments.ts +0 -0
- /package/{src → dist}/app/routes/board/handlers/files.ts +0 -0
- /package/{src → dist}/app/routes/board/handlers/likes.ts +0 -0
- /package/{src → dist}/app/routes/board/handlers/mentions.ts +0 -0
- /package/{src → dist}/app/routes/board/handlers/posts.ts +0 -0
- /package/{src → dist}/app/routes/board/handlers/ratings.ts +0 -0
- /package/{src → dist}/app/routes/board/handlers/reports.ts +0 -0
- /package/{src → dist}/app/routes/board/handlers/tags.ts +0 -0
- /package/{src → dist}/app/routes/board/routes.ts +0 -0
- /package/{src → dist}/app/routes/password-reset/config.example.json +0 -0
- /package/{src → dist}/app/routes/password-reset/config.json +0 -0
- /package/{src → dist}/app/routes/password-reset/entities/account.json +0 -0
- /package/{src → dist}/app/routes/password-reset/handlers.ts +0 -0
- /package/{src → dist}/app/routes/password-reset/password-utils.ts +0 -0
- /package/{src → dist}/app/routes/password-reset/routes.ts +0 -0
- /package/{src → dist}/app/routes/password-reset/templates/password_reset.html +0 -0
- /package/{src → dist}/app/routes/password-reset/templates/password_reset_link.html +0 -0
- /package/{src → dist}/app/routes/password-reset/types/index.ts +0 -0
- /package/{src → dist}/app/routes/privilege/config.json +0 -0
- /package/{src → dist}/app/routes/privilege/entities/pv_group.json +0 -0
- /package/{src → dist}/app/routes/privilege/entities/pv_group_item.json +0 -0
- /package/{src → dist}/app/routes/privilege/entities/pv_item.json +0 -0
- /package/{src → dist}/app/routes/privilege/entities/user_pv_group.json +0 -0
- /package/{src → dist}/app/routes/privilege/entities/user_pv_item.json +0 -0
- /package/{src → dist}/app/routes/privilege/routes.ts +0 -0
- /package/{src → dist}/app/routes/user/config.json +0 -0
- /package/{src → dist}/app/routes/user/entities/user.json +0 -0
- /package/{src → dist}/app/routes/user/entities/user_biometric.json +0 -0
- /package/{src → dist}/app/routes/user/routes.ts +0 -0
- /package/{src → dist}/app/schedules/README.md +0 -0
- /package/{src → dist}/app/schedules/data-retention/config.json +0 -0
- /package/{src → dist}/app/schedules/data-retention/index.ts +0 -0
- /package/{src → dist}/app/schedules/dormancy/config.json +0 -0
- /package/{src → dist}/app/schedules/dormancy/entities/account.json +0 -0
- /package/{src → dist}/app/schedules/dormancy/entities/privacy_cron_lock.json +0 -0
- /package/{src → dist}/app/schedules/dormancy/index.ts +0 -0
- /package/{src → dist}/app/schedules/dormancy/templates/dormancy_completed.html +0 -0
- /package/{src → dist}/app/schedules/dormancy/templates/dormancy_warning.html +0 -0
- /package/{configs → dist/configs}/cache.json +0 -0
- /package/{configs → dist/configs}/cors.json +0 -0
- /package/{configs → dist/configs}/database.json +0 -0
- /package/{configs → dist/configs}/security.json +0 -0
- /package/{configs → dist/configs}/server.json +0 -0
- /package/{docs → dist/docs}/README.md +0 -0
- /package/{docs → dist/docs}/architecture.md +0 -0
- /package/{docs → dist/docs}/cache.md +0 -0
- /package/{docs → dist/docs}/configs.md +0 -0
- /package/{docs → dist/docs}/database.md +0 -0
- /package/{docs → dist/docs}/flows.md +0 -0
- /package/{docs → dist/docs}/getting-started.md +0 -0
- /package/{docs → dist/docs}/hooks.md +0 -0
- /package/{docs → dist/docs}/internals.md +0 -0
- /package/{docs → dist/docs}/plugins/2fa.md +0 -0
- /package/{docs → dist/docs}/plugins/alimtalk.md +0 -0
- /package/{docs → dist/docs}/plugins/friendtalk.md +0 -0
- /package/{docs → dist/docs}/plugins/holidays.md +0 -0
- /package/{docs → dist/docs}/plugins/how-to-create.md +0 -0
- /package/{docs → dist/docs}/plugins/identity.md +0 -0
- /package/{docs → dist/docs}/plugins/llm.md +0 -0
- /package/{docs → dist/docs}/plugins/oauth.md +0 -0
- /package/{docs → dist/docs}/plugins/ocr.md +0 -0
- /package/{docs → dist/docs}/plugins/pg.md +0 -0
- /package/{docs → dist/docs}/plugins/push.md +0 -0
- /package/{docs → dist/docs}/plugins/sms.md +0 -0
- /package/{docs → dist/docs}/plugins/taxinvoice.md +0 -0
- /package/{docs → dist/docs}/routes/README.md +0 -0
- /package/{docs → dist/docs}/routes/account-routes.md +0 -0
- /package/{docs → dist/docs}/routes/alimtalk-routes.md +0 -0
- /package/{docs → dist/docs}/routes/board-routes.md +0 -0
- /package/{docs → dist/docs}/routes/email-verification.md +0 -0
- /package/{docs → dist/docs}/routes/friendtalk-routes.md +0 -0
- /package/{docs → dist/docs}/routes/holidays-routes.md +0 -0
- /package/{docs → dist/docs}/routes/how-to-create.md +0 -0
- /package/{docs → dist/docs}/routes/identity-routes.md +0 -0
- /package/{docs → dist/docs}/routes/llm-routes.md +0 -0
- /package/{docs → dist/docs}/routes/ocr-routes.md +0 -0
- /package/{docs → dist/docs}/routes/password-reset.md +0 -0
- /package/{docs → dist/docs}/routes/pg-routes.md +0 -0
- /package/{docs → dist/docs}/routes/push-routes.md +0 -0
- /package/{docs → dist/docs}/routes/sms-routes.md +0 -0
- /package/{docs → dist/docs}/routes/smtp-routes.md +0 -0
- /package/{docs → dist/docs}/routes/tax-invoice-routes.md +0 -0
- /package/{docs → dist/docs}/schedules/dormancy-and-retention.md +0 -0
- /package/{docs → dist/docs}/schedules/how-to-create.md +0 -0
- /package/{docs → dist/docs}/scripts-guide.md +0 -0
- /package/{docs → dist/docs}/security.md +0 -0
- /package/{docs → dist/docs}/system.md +0 -0
- /package/{scripts → dist/scripts}/entity.sh +0 -0
- /package/{scripts → dist/scripts}/reset-all.sh +0 -0
- /package/{scripts → dist/scripts}/run.sh +0 -0
- /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`)
|