@kurly-growth/growthman 0.1.13 → 0.1.14
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/CLAUDE.md +58 -0
- package/app/globals.css +125 -0
- package/app/layout.tsx +36 -0
- package/app/page.tsx +213 -0
- package/bin/cli.js +12 -2
- package/components/api-test-dialog.tsx +222 -0
- package/components/endpoint-edit-dialog.tsx +181 -0
- package/components/endpoint-table.tsx +147 -0
- package/components/openapi-upload-dialog.tsx +213 -0
- package/components/ui/button.tsx +62 -0
- package/components/ui/checkbox.tsx +32 -0
- package/components/ui/dialog.tsx +143 -0
- package/components/ui/input.tsx +21 -0
- package/components/ui/label.tsx +24 -0
- package/components/ui/sonner.tsx +37 -0
- package/components/ui/table.tsx +116 -0
- package/components/ui/textarea.tsx +18 -0
- package/components.json +22 -0
- package/next-env.d.ts +6 -0
- package/package.json +10 -19
- package/pnpm-workspace.yaml +4 -0
- package/postcss.config.mjs +7 -0
- package/prisma/prisma/dev.db +0 -0
- package/.next/BUILD_ID +0 -1
- package/.next/app-path-routes-manifest.json +0 -12
- package/.next/build/chunks/[root-of-the-server]__51225daf._.js +0 -206
- package/.next/build/chunks/[root-of-the-server]__51225daf._.js.map +0 -8
- package/.next/build/chunks/[root-of-the-server]__974941ed._.js +0 -500
- package/.next/build/chunks/[root-of-the-server]__974941ed._.js.map +0 -11
- package/.next/build/chunks/[turbopack-node]_transforms_postcss_ts_6920245c._.js +0 -13
- package/.next/build/chunks/[turbopack-node]_transforms_postcss_ts_6920245c._.js.map +0 -5
- package/.next/build/chunks/[turbopack]_runtime.js +0 -795
- package/.next/build/chunks/[turbopack]_runtime.js.map +0 -10
- package/.next/build/chunks/node_modules_fe693df6._.js +0 -6758
- package/.next/build/chunks/node_modules_fe693df6._.js.map +0 -47
- package/.next/build/package.json +0 -1
- package/.next/build/postcss.js +0 -6
- package/.next/build/postcss.js.map +0 -5
- package/.next/build-manifest.json +0 -19
- package/.next/diagnostics/build-diagnostics.json +0 -6
- package/.next/diagnostics/framework.json +0 -1
- package/.next/export-marker.json +0 -6
- package/.next/fallback-build-manifest.json +0 -12
- package/.next/images-manifest.json +0 -66
- package/.next/next-minimal-server.js.nft.json +0 -1
- package/.next/next-server.js.nft.json +0 -1
- package/.next/package.json +0 -1
- package/.next/prerender-manifest.json +0 -114
- package/.next/required-server-files.js +0 -163
- package/.next/required-server-files.json +0 -163
- package/.next/routes-manifest.json +0 -109
- package/.next/server/app/_global-error/page/app-paths-manifest.json +0 -3
- package/.next/server/app/_global-error/page/build-manifest.json +0 -16
- package/.next/server/app/_global-error/page/next-font-manifest.json +0 -6
- package/.next/server/app/_global-error/page/react-loadable-manifest.json +0 -1
- package/.next/server/app/_global-error/page/server-reference-manifest.json +0 -4
- package/.next/server/app/_global-error/page.js +0 -11
- package/.next/server/app/_global-error/page.js.map +0 -5
- package/.next/server/app/_global-error/page.js.nft.json +0 -1
- package/.next/server/app/_global-error/page_client-reference-manifest.js +0 -2
- package/.next/server/app/_global-error.html +0 -2
- package/.next/server/app/_global-error.meta +0 -15
- package/.next/server/app/_global-error.rsc +0 -13
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +0 -5
- package/.next/server/app/_global-error.segments/_full.segment.rsc +0 -13
- package/.next/server/app/_global-error.segments/_head.segment.rsc +0 -6
- package/.next/server/app/_global-error.segments/_index.segment.rsc +0 -4
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +0 -1
- package/.next/server/app/_not-found/page/app-paths-manifest.json +0 -3
- package/.next/server/app/_not-found/page/build-manifest.json +0 -16
- package/.next/server/app/_not-found/page/next-font-manifest.json +0 -11
- package/.next/server/app/_not-found/page/react-loadable-manifest.json +0 -1
- package/.next/server/app/_not-found/page/server-reference-manifest.json +0 -4
- package/.next/server/app/_not-found/page.js +0 -14
- package/.next/server/app/_not-found/page.js.map +0 -5
- package/.next/server/app/_not-found/page.js.nft.json +0 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +0 -2
- package/.next/server/app/_not-found.html +0 -1
- package/.next/server/app/_not-found.meta +0 -16
- package/.next/server/app/_not-found.rsc +0 -15
- package/.next/server/app/_not-found.segments/_full.segment.rsc +0 -15
- package/.next/server/app/_not-found.segments/_head.segment.rsc +0 -6
- package/.next/server/app/_not-found.segments/_index.segment.rsc +0 -6
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +0 -5
- package/.next/server/app/_not-found.segments/_not-found.segment.rsc +0 -4
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +0 -2
- package/.next/server/app/api/endpoints/[id]/route/app-paths-manifest.json +0 -3
- package/.next/server/app/api/endpoints/[id]/route/build-manifest.json +0 -11
- package/.next/server/app/api/endpoints/[id]/route/server-reference-manifest.json +0 -4
- package/.next/server/app/api/endpoints/[id]/route.js +0 -7
- package/.next/server/app/api/endpoints/[id]/route.js.map +0 -5
- package/.next/server/app/api/endpoints/[id]/route.js.nft.json +0 -1
- package/.next/server/app/api/endpoints/[id]/route_client-reference-manifest.js +0 -2
- package/.next/server/app/api/endpoints/bulk/route/app-paths-manifest.json +0 -3
- package/.next/server/app/api/endpoints/bulk/route/build-manifest.json +0 -11
- package/.next/server/app/api/endpoints/bulk/route/server-reference-manifest.json +0 -4
- package/.next/server/app/api/endpoints/bulk/route.js +0 -7
- package/.next/server/app/api/endpoints/bulk/route.js.map +0 -5
- package/.next/server/app/api/endpoints/bulk/route.js.nft.json +0 -1
- package/.next/server/app/api/endpoints/bulk/route_client-reference-manifest.js +0 -2
- package/.next/server/app/api/endpoints/import/route/app-paths-manifest.json +0 -3
- package/.next/server/app/api/endpoints/import/route/build-manifest.json +0 -11
- package/.next/server/app/api/endpoints/import/route/server-reference-manifest.json +0 -4
- package/.next/server/app/api/endpoints/import/route.js +0 -7
- package/.next/server/app/api/endpoints/import/route.js.map +0 -5
- package/.next/server/app/api/endpoints/import/route.js.nft.json +0 -1
- package/.next/server/app/api/endpoints/import/route_client-reference-manifest.js +0 -2
- package/.next/server/app/api/endpoints/route/app-paths-manifest.json +0 -3
- package/.next/server/app/api/endpoints/route/build-manifest.json +0 -11
- package/.next/server/app/api/endpoints/route/server-reference-manifest.json +0 -4
- package/.next/server/app/api/endpoints/route.js +0 -7
- package/.next/server/app/api/endpoints/route.js.map +0 -5
- package/.next/server/app/api/endpoints/route.js.nft.json +0 -1
- package/.next/server/app/api/endpoints/route_client-reference-manifest.js +0 -2
- package/.next/server/app/api/endpoints/sync/route/app-paths-manifest.json +0 -3
- package/.next/server/app/api/endpoints/sync/route/build-manifest.json +0 -11
- package/.next/server/app/api/endpoints/sync/route/server-reference-manifest.json +0 -4
- package/.next/server/app/api/endpoints/sync/route.js +0 -7
- package/.next/server/app/api/endpoints/sync/route.js.map +0 -5
- package/.next/server/app/api/endpoints/sync/route.js.nft.json +0 -1
- package/.next/server/app/api/endpoints/sync/route_client-reference-manifest.js +0 -2
- package/.next/server/app/api/mock/[...slug]/route/app-paths-manifest.json +0 -3
- package/.next/server/app/api/mock/[...slug]/route/build-manifest.json +0 -11
- package/.next/server/app/api/mock/[...slug]/route/server-reference-manifest.json +0 -4
- package/.next/server/app/api/mock/[...slug]/route.js +0 -8
- package/.next/server/app/api/mock/[...slug]/route.js.map +0 -5
- package/.next/server/app/api/mock/[...slug]/route.js.nft.json +0 -1
- package/.next/server/app/api/mock/[...slug]/route_client-reference-manifest.js +0 -2
- package/.next/server/app/favicon.ico/route/app-paths-manifest.json +0 -3
- package/.next/server/app/favicon.ico/route/build-manifest.json +0 -11
- package/.next/server/app/favicon.ico/route.js +0 -7
- package/.next/server/app/favicon.ico/route.js.map +0 -5
- package/.next/server/app/favicon.ico/route.js.nft.json +0 -1
- package/.next/server/app/favicon.ico.meta +0 -1
- package/.next/server/app/index.html +0 -1
- package/.next/server/app/index.meta +0 -14
- package/.next/server/app/index.rsc +0 -21
- package/.next/server/app/index.segments/__PAGE__.segment.rsc +0 -9
- package/.next/server/app/index.segments/_full.segment.rsc +0 -21
- package/.next/server/app/index.segments/_head.segment.rsc +0 -6
- package/.next/server/app/index.segments/_index.segment.rsc +0 -6
- package/.next/server/app/index.segments/_tree.segment.rsc +0 -4
- package/.next/server/app/page/app-paths-manifest.json +0 -3
- package/.next/server/app/page/build-manifest.json +0 -16
- package/.next/server/app/page/next-font-manifest.json +0 -11
- package/.next/server/app/page/react-loadable-manifest.json +0 -8
- package/.next/server/app/page/server-reference-manifest.json +0 -4
- package/.next/server/app/page.js +0 -16
- package/.next/server/app/page.js.map +0 -5
- package/.next/server/app/page.js.nft.json +0 -1
- package/.next/server/app/page_client-reference-manifest.js +0 -2
- package/.next/server/app-paths-manifest.json +0 -12
- package/.next/server/chunks/1629d_next_dist_esm_build_templates_app-route_498527d5.js +0 -3
- package/.next/server/chunks/1629d_next_dist_esm_build_templates_app-route_498527d5.js.map +0 -1
- package/.next/server/chunks/[externals]__cf2ccb51._.js +0 -3
- package/.next/server/chunks/[externals]__cf2ccb51._.js.map +0 -1
- package/.next/server/chunks/[externals]_next_dist_8dbe5856._.js +0 -3
- package/.next/server/chunks/[externals]_next_dist_8dbe5856._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__3534fecc._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__3534fecc._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__461ea613._.js +0 -21
- package/.next/server/chunks/[root-of-the-server]__461ea613._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__4929acb0._.js +0 -129
- package/.next/server/chunks/[root-of-the-server]__4929acb0._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__909218aa._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__909218aa._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__a6c01a13._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__a6c01a13._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__db1127cf._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__db1127cf._.js.map +0 -1
- package/.next/server/chunks/[root-of-the-server]__e46f3e25._.js +0 -3
- package/.next/server/chunks/[root-of-the-server]__e46f3e25._.js.map +0 -1
- package/.next/server/chunks/[turbopack]_runtime.js +0 -795
- package/.next/server/chunks/[turbopack]_runtime.js.map +0 -10
- package/.next/server/chunks/_next-internal_server_app_api_endpoints_[id]_route_actions_b91cfc4c.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_endpoints_[id]_route_actions_b91cfc4c.js.map +0 -1
- package/.next/server/chunks/_next-internal_server_app_api_endpoints_bulk_route_actions_560cc6cd.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_endpoints_bulk_route_actions_560cc6cd.js.map +0 -1
- package/.next/server/chunks/_next-internal_server_app_api_endpoints_import_route_actions_f2444950.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_endpoints_import_route_actions_f2444950.js.map +0 -1
- package/.next/server/chunks/_next-internal_server_app_api_endpoints_route_actions_49d8ad56.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_endpoints_route_actions_49d8ad56.js.map +0 -1
- package/.next/server/chunks/_next-internal_server_app_api_endpoints_sync_route_actions_0f446550.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_endpoints_sync_route_actions_0f446550.js.map +0 -1
- package/.next/server/chunks/_next-internal_server_app_api_mock_[___slug]_route_actions_be875f77.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_api_mock_[___slug]_route_actions_be875f77.js.map +0 -1
- package/.next/server/chunks/_next-internal_server_app_favicon_ico_route_actions_353150a5.js +0 -3
- package/.next/server/chunks/_next-internal_server_app_favicon_ico_route_actions_353150a5.js.map +0 -1
- package/.next/server/chunks/node_modules__pnpm_a61fb769._.js +0 -3
- package/.next/server/chunks/node_modules__pnpm_a61fb769._.js.map +0 -1
- package/.next/server/chunks/ssr/1629d_next_dist_1a21bde7._.js +0 -6
- package/.next/server/chunks/ssr/1629d_next_dist_1a21bde7._.js.map +0 -1
- package/.next/server/chunks/ssr/1629d_next_dist_8dc31fba._.js +0 -3
- package/.next/server/chunks/ssr/1629d_next_dist_8dc31fba._.js.map +0 -1
- package/.next/server/chunks/ssr/1629d_next_dist_client_components_b01b33e4._.js +0 -3
- package/.next/server/chunks/ssr/1629d_next_dist_client_components_b01b33e4._.js.map +0 -1
- package/.next/server/chunks/ssr/1629d_next_dist_client_components_builtin_forbidden_4cab2078.js +0 -3
- package/.next/server/chunks/ssr/1629d_next_dist_client_components_builtin_forbidden_4cab2078.js.map +0 -1
- package/.next/server/chunks/ssr/1629d_next_dist_client_components_builtin_global-error_0d5cb623.js +0 -3
- package/.next/server/chunks/ssr/1629d_next_dist_client_components_builtin_global-error_0d5cb623.js.map +0 -1
- package/.next/server/chunks/ssr/1629d_next_dist_client_components_builtin_unauthorized_b8fbdcad.js +0 -3
- package/.next/server/chunks/ssr/1629d_next_dist_client_components_builtin_unauthorized_b8fbdcad.js.map +0 -1
- package/.next/server/chunks/ssr/1629d_next_dist_d78dee57._.js +0 -4
- package/.next/server/chunks/ssr/1629d_next_dist_d78dee57._.js.map +0 -1
- package/.next/server/chunks/ssr/1629d_next_dist_esm_build_templates_app-page_d4e9464b.js +0 -4
- package/.next/server/chunks/ssr/1629d_next_dist_esm_build_templates_app-page_d4e9464b.js.map +0 -1
- package/.next/server/chunks/ssr/67049_lucide-react_dist_esm_createLucideIcon_22fe2e14.js +0 -3
- package/.next/server/chunks/ssr/67049_lucide-react_dist_esm_createLucideIcon_22fe2e14.js.map +0 -1
- package/.next/server/chunks/ssr/[externals]_next_dist_server_app-render_work-async-storage_external_1f8eeae7.js +0 -3
- package/.next/server/chunks/ssr/[externals]_next_dist_server_app-render_work-async-storage_external_1f8eeae7.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__14f4396a._.js +0 -10
- package/.next/server/chunks/ssr/[root-of-the-server]__14f4396a._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__3064bf15._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__3064bf15._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__4ff41ba3._.js +0 -4
- package/.next/server/chunks/ssr/[root-of-the-server]__4ff41ba3._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__57c5da8e._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__57c5da8e._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__67653b38._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__67653b38._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__7d48410d._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__7d48410d._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__a49eaf36._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__a49eaf36._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__b0617f51._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__b0617f51._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__cc026bde._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__cc026bde._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__d079898e._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__d079898e._.js.map +0 -1
- package/.next/server/chunks/ssr/[root-of-the-server]__d3649e47._.js +0 -3
- package/.next/server/chunks/ssr/[root-of-the-server]__d3649e47._.js.map +0 -1
- package/.next/server/chunks/ssr/[turbopack]_runtime.js +0 -795
- package/.next/server/chunks/ssr/[turbopack]_runtime.js.map +0 -10
- package/.next/server/chunks/ssr/_9b6e3dc4._.js +0 -3
- package/.next/server/chunks/ssr/_9b6e3dc4._.js.map +0 -1
- package/.next/server/chunks/ssr/_a437ac52._.js +0 -7
- package/.next/server/chunks/ssr/_a437ac52._.js.map +0 -1
- package/.next/server/chunks/ssr/_b62b070d._.js +0 -4
- package/.next/server/chunks/ssr/_b62b070d._.js.map +0 -1
- package/.next/server/chunks/ssr/_next-internal_server_app__global-error_page_actions_75761787.js +0 -3
- package/.next/server/chunks/ssr/_next-internal_server_app__global-error_page_actions_75761787.js.map +0 -1
- package/.next/server/chunks/ssr/_next-internal_server_app__not-found_page_actions_554ec2bf.js +0 -3
- package/.next/server/chunks/ssr/_next-internal_server_app__not-found_page_actions_554ec2bf.js.map +0 -1
- package/.next/server/chunks/ssr/_next-internal_server_app_page_actions_39d4fc33.js +0 -3
- package/.next/server/chunks/ssr/_next-internal_server_app_page_actions_39d4fc33.js.map +0 -1
- package/.next/server/chunks/ssr/app_b9b1292a._.js +0 -3
- package/.next/server/chunks/ssr/app_b9b1292a._.js.map +0 -1
- package/.next/server/functions-config-manifest.json +0 -4
- package/.next/server/interception-route-rewrite-manifest.js +0 -1
- package/.next/server/middleware-build-manifest.js +0 -20
- package/.next/server/middleware-manifest.json +0 -6
- package/.next/server/next-font-manifest.js +0 -1
- package/.next/server/next-font-manifest.json +0 -15
- package/.next/server/pages/404.html +0 -1
- package/.next/server/pages/500.html +0 -2
- package/.next/server/pages-manifest.json +0 -4
- package/.next/server/server-reference-manifest.js +0 -1
- package/.next/server/server-reference-manifest.json +0 -5
- package/.next/static/chunks/16403d658c649f0f.js +0 -1
- package/.next/static/chunks/2422cfacfdb28c2c.js +0 -5
- package/.next/static/chunks/42572c067be8ea0f.js +0 -1
- package/.next/static/chunks/5045da71379799ce.js +0 -1
- package/.next/static/chunks/6e04dfc4035d7150.js +0 -5
- package/.next/static/chunks/8155485116e3ff24.js +0 -1
- package/.next/static/chunks/a66e09a7c2336f67.js +0 -1
- package/.next/static/chunks/a6dad97d9634a72d.js +0 -1
- package/.next/static/chunks/a6dad97d9634a72d.js.map +0 -1
- package/.next/static/chunks/da3f3e4f37f68cee.css +0 -3
- package/.next/static/chunks/f1cfb69226717279.js +0 -1
- package/.next/static/chunks/turbopack-7027959231bb432a.js +0 -4
- package/.next/static/media/4fa387ec64143e14-s.c1fdd6c2.woff2 +0 -0
- package/.next/static/media/7178b3e590c64307-s.b97b3418.woff2 +0 -0
- package/.next/static/media/797e433ab948586e-s.p.dbea232f.woff2 +0 -0
- package/.next/static/media/8a480f0b521d4e75-s.8e0177b5.woff2 +0 -0
- package/.next/static/media/bbc41e54d2fcbd21-s.799d8ef8.woff2 +0 -0
- package/.next/static/media/caa3a2e1cccd8315-s.p.853070df.woff2 +0 -0
- package/.next/static/media/favicon.de6b30f1.ico +0 -0
- package/.next/static/rD3QdLXPN9C3vquucAUu6/_buildManifest.js +0 -11
- package/.next/static/rD3QdLXPN9C3vquucAUu6/_clientMiddlewareManifest.json +0 -1
- package/.next/static/rD3QdLXPN9C3vquucAUu6/_ssgManifest.js +0 -1
- package/.next/trace +0 -1
- package/.next/trace-build +0 -1
- package/.next/turbopack +0 -0
- package/.next/types/routes.d.ts +0 -78
- package/.next/types/validator.ts +0 -124
- /package/{.next/server/app/favicon.ico.body → app/favicon.ico} +0 -0
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Development Commands
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm dev # Start dev server (http://localhost:3100)
|
|
9
|
+
pnpm dev:clean # Reset DB with seed data and start dev server
|
|
10
|
+
pnpm build # Build for production
|
|
11
|
+
pnpm lint # Run ESLint
|
|
12
|
+
pnpm db:seed # Seed the database
|
|
13
|
+
pnpm db:reset # Reset database with migrations
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Architecture Overview
|
|
17
|
+
|
|
18
|
+
This is a **Mock Server Dashboard** - a Next.js 16 application that allows users to create, manage, and serve mock API endpoints stored in PostgreSQL via Prisma.
|
|
19
|
+
|
|
20
|
+
### Core Concept
|
|
21
|
+
|
|
22
|
+
1. Users define mock endpoints (path, method, status code, response body) through a web UI
|
|
23
|
+
2. These endpoints are stored in the `MockEndpoint` table
|
|
24
|
+
3. Requests to `/api/mock/*` are dynamically matched against stored endpoints using `path-to-regexp`
|
|
25
|
+
|
|
26
|
+
### Key Components
|
|
27
|
+
|
|
28
|
+
- **`/app/api/mock/[...slug]/route.ts`** - Catch-all route that handles all mock requests. Matches incoming requests against stored endpoints using pattern matching (supports path parameters like `:id`)
|
|
29
|
+
|
|
30
|
+
- **`/app/api/endpoints/`** - CRUD API for managing mock endpoints
|
|
31
|
+
- `route.ts` - GET (list all), POST (create)
|
|
32
|
+
- `[id]/route.ts` - PUT (update), DELETE
|
|
33
|
+
- `import/route.ts` - Bulk import from OpenAPI spec
|
|
34
|
+
- `sync/route.ts` - Sync endpoints with external spec
|
|
35
|
+
|
|
36
|
+
- **`/lib/openapi-parser.ts`** - Parses OpenAPI 3.x specs and generates mock response data based on schema definitions. Handles `$ref`, `allOf`, `oneOf`, `anyOf`, and generates type-appropriate mock values.
|
|
37
|
+
|
|
38
|
+
- **`/components/`** - React components for the dashboard UI
|
|
39
|
+
- `endpoint-table.tsx` - Display list of endpoints
|
|
40
|
+
- `endpoint-edit-dialog.tsx` - Create/edit endpoint with Monaco editor for JSON
|
|
41
|
+
- `openapi-upload-dialog.tsx` - Import endpoints from OpenAPI spec
|
|
42
|
+
|
|
43
|
+
### Database Schema
|
|
44
|
+
|
|
45
|
+
Single table `MockEndpoint` with unique constraint on `(path, method)`:
|
|
46
|
+
- `path` - Express-style route (e.g., `/v1/users/:id`)
|
|
47
|
+
- `method` - HTTP method
|
|
48
|
+
- `statusCode` - Response status code
|
|
49
|
+
- `responseBody` - JSON response data
|
|
50
|
+
|
|
51
|
+
### Tech Stack
|
|
52
|
+
|
|
53
|
+
- Next.js 16 (App Router)
|
|
54
|
+
- React 19
|
|
55
|
+
- Prisma with PostgreSQL
|
|
56
|
+
- Tailwind CSS 4
|
|
57
|
+
- Radix UI + shadcn/ui components
|
|
58
|
+
- Monaco Editor for JSON editing
|
package/app/globals.css
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
|
|
4
|
+
@custom-variant dark (&:is(.dark *));
|
|
5
|
+
|
|
6
|
+
@theme inline {
|
|
7
|
+
--color-background: var(--background);
|
|
8
|
+
--color-foreground: var(--foreground);
|
|
9
|
+
--font-sans: var(--font-geist-sans);
|
|
10
|
+
--font-mono: var(--font-geist-mono);
|
|
11
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
12
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
13
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
14
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
15
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
16
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
17
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
18
|
+
--color-sidebar: var(--sidebar);
|
|
19
|
+
--color-chart-5: var(--chart-5);
|
|
20
|
+
--color-chart-4: var(--chart-4);
|
|
21
|
+
--color-chart-3: var(--chart-3);
|
|
22
|
+
--color-chart-2: var(--chart-2);
|
|
23
|
+
--color-chart-1: var(--chart-1);
|
|
24
|
+
--color-ring: var(--ring);
|
|
25
|
+
--color-input: var(--input);
|
|
26
|
+
--color-border: var(--border);
|
|
27
|
+
--color-destructive: var(--destructive);
|
|
28
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
29
|
+
--color-accent: var(--accent);
|
|
30
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
31
|
+
--color-muted: var(--muted);
|
|
32
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
33
|
+
--color-secondary: var(--secondary);
|
|
34
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
35
|
+
--color-primary: var(--primary);
|
|
36
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
37
|
+
--color-popover: var(--popover);
|
|
38
|
+
--color-card-foreground: var(--card-foreground);
|
|
39
|
+
--color-card: var(--card);
|
|
40
|
+
--radius-sm: calc(var(--radius) - 4px);
|
|
41
|
+
--radius-md: calc(var(--radius) - 2px);
|
|
42
|
+
--radius-lg: var(--radius);
|
|
43
|
+
--radius-xl: calc(var(--radius) + 4px);
|
|
44
|
+
--radius-2xl: calc(var(--radius) + 8px);
|
|
45
|
+
--radius-3xl: calc(var(--radius) + 12px);
|
|
46
|
+
--radius-4xl: calc(var(--radius) + 16px);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
:root {
|
|
50
|
+
--radius: 0.625rem;
|
|
51
|
+
--background: oklch(1 0 0);
|
|
52
|
+
--foreground: oklch(0.145 0 0);
|
|
53
|
+
--card: oklch(1 0 0);
|
|
54
|
+
--card-foreground: oklch(0.145 0 0);
|
|
55
|
+
--popover: oklch(1 0 0);
|
|
56
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
57
|
+
--primary: oklch(0.205 0 0);
|
|
58
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
59
|
+
--secondary: oklch(0.97 0 0);
|
|
60
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
61
|
+
--muted: oklch(0.97 0 0);
|
|
62
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
63
|
+
--accent: oklch(0.97 0 0);
|
|
64
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
65
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
66
|
+
--border: oklch(0.922 0 0);
|
|
67
|
+
--input: oklch(0.922 0 0);
|
|
68
|
+
--ring: oklch(0.708 0 0);
|
|
69
|
+
--chart-1: oklch(0.646 0.222 41.116);
|
|
70
|
+
--chart-2: oklch(0.6 0.118 184.704);
|
|
71
|
+
--chart-3: oklch(0.398 0.07 227.392);
|
|
72
|
+
--chart-4: oklch(0.828 0.189 84.429);
|
|
73
|
+
--chart-5: oklch(0.769 0.188 70.08);
|
|
74
|
+
--sidebar: oklch(0.985 0 0);
|
|
75
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
76
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
77
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
78
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
79
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
80
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
81
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.dark {
|
|
85
|
+
--background: oklch(0.145 0 0);
|
|
86
|
+
--foreground: oklch(0.985 0 0);
|
|
87
|
+
--card: oklch(0.205 0 0);
|
|
88
|
+
--card-foreground: oklch(0.985 0 0);
|
|
89
|
+
--popover: oklch(0.205 0 0);
|
|
90
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
91
|
+
--primary: oklch(0.922 0 0);
|
|
92
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
93
|
+
--secondary: oklch(0.269 0 0);
|
|
94
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
95
|
+
--muted: oklch(0.269 0 0);
|
|
96
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
97
|
+
--accent: oklch(0.269 0 0);
|
|
98
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
99
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
100
|
+
--border: oklch(1 0 0 / 10%);
|
|
101
|
+
--input: oklch(1 0 0 / 15%);
|
|
102
|
+
--ring: oklch(0.556 0 0);
|
|
103
|
+
--chart-1: oklch(0.488 0.243 264.376);
|
|
104
|
+
--chart-2: oklch(0.696 0.17 162.48);
|
|
105
|
+
--chart-3: oklch(0.769 0.188 70.08);
|
|
106
|
+
--chart-4: oklch(0.627 0.265 303.9);
|
|
107
|
+
--chart-5: oklch(0.645 0.246 16.439);
|
|
108
|
+
--sidebar: oklch(0.205 0 0);
|
|
109
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
110
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
111
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
112
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
113
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
114
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
115
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@layer base {
|
|
119
|
+
* {
|
|
120
|
+
@apply border-border outline-ring/50;
|
|
121
|
+
}
|
|
122
|
+
body {
|
|
123
|
+
@apply bg-background text-foreground;
|
|
124
|
+
}
|
|
125
|
+
}
|
package/app/layout.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Geist, Geist_Mono } from "next/font/google";
|
|
3
|
+
import { Toaster } from "@/components/ui/sonner";
|
|
4
|
+
import "./globals.css";
|
|
5
|
+
|
|
6
|
+
const geistSans = Geist({
|
|
7
|
+
variable: "--font-geist-sans",
|
|
8
|
+
subsets: ["latin"],
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const geistMono = Geist_Mono({
|
|
12
|
+
variable: "--font-geist-mono",
|
|
13
|
+
subsets: ["latin"],
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const metadata: Metadata = {
|
|
17
|
+
title: "Growthman",
|
|
18
|
+
description: "Local mock API server with UI dashboard",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default function RootLayout({
|
|
22
|
+
children,
|
|
23
|
+
}: Readonly<{
|
|
24
|
+
children: React.ReactNode;
|
|
25
|
+
}>) {
|
|
26
|
+
return (
|
|
27
|
+
<html lang="en">
|
|
28
|
+
<body
|
|
29
|
+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
|
30
|
+
>
|
|
31
|
+
{children}
|
|
32
|
+
<Toaster />
|
|
33
|
+
</body>
|
|
34
|
+
</html>
|
|
35
|
+
);
|
|
36
|
+
}
|
package/app/page.tsx
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react'
|
|
4
|
+
import { MockEndpoint } from '@/types/endpoint'
|
|
5
|
+
import { EndpointTable } from '@/components/endpoint-table'
|
|
6
|
+
import { EndpointEditDialog } from '@/components/endpoint-edit-dialog'
|
|
7
|
+
import { OpenAPIUploadDialog } from '@/components/openapi-upload-dialog'
|
|
8
|
+
import { ApiTestDialog } from '@/components/api-test-dialog'
|
|
9
|
+
import { Button } from '@/components/ui/button'
|
|
10
|
+
import { toast } from 'sonner'
|
|
11
|
+
|
|
12
|
+
export default function Home() {
|
|
13
|
+
const [endpoints, setEndpoints] = useState<MockEndpoint[]>([])
|
|
14
|
+
const [loading, setLoading] = useState(true)
|
|
15
|
+
const [editDialogOpen, setEditDialogOpen] = useState(false)
|
|
16
|
+
const [uploadDialogOpen, setUploadDialogOpen] = useState(false)
|
|
17
|
+
const [testDialogOpen, setTestDialogOpen] = useState(false)
|
|
18
|
+
const [selectedEndpoint, setSelectedEndpoint] = useState<MockEndpoint | null>(null)
|
|
19
|
+
const [testEndpoint, setTestEndpoint] = useState<MockEndpoint | null>(null)
|
|
20
|
+
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set())
|
|
21
|
+
const [baseUrl, setBaseUrl] = useState('')
|
|
22
|
+
const [lastModifiedId, setLastModifiedId] = useState<string | null>(null)
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setBaseUrl(window.location.origin)
|
|
26
|
+
}, [])
|
|
27
|
+
|
|
28
|
+
const fetchEndpoints = useCallback(async () => {
|
|
29
|
+
try {
|
|
30
|
+
const response = await fetch('/api/endpoints')
|
|
31
|
+
const data = await response.json()
|
|
32
|
+
setEndpoints(data)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('Failed to fetch endpoints:', error)
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false)
|
|
37
|
+
}
|
|
38
|
+
}, [])
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
fetchEndpoints()
|
|
42
|
+
}, [fetchEndpoints])
|
|
43
|
+
|
|
44
|
+
const handleEdit = (endpoint: MockEndpoint) => {
|
|
45
|
+
setSelectedEndpoint(endpoint)
|
|
46
|
+
setEditDialogOpen(true)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const handleCreate = () => {
|
|
50
|
+
setSelectedEndpoint(null)
|
|
51
|
+
setEditDialogOpen(true)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const handleTest = (endpoint: MockEndpoint) => {
|
|
55
|
+
setTestEndpoint(endpoint)
|
|
56
|
+
setTestDialogOpen(true)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const handleDelete = async (id: string) => {
|
|
60
|
+
if (!confirm('Are you sure you want to delete this endpoint?')) return
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
const response = await fetch(`/api/endpoints/${id}`, { method: 'DELETE' })
|
|
64
|
+
if (!response.ok) throw new Error('Failed to delete')
|
|
65
|
+
setSelectedIds((prev) => {
|
|
66
|
+
const newSet = new Set(prev)
|
|
67
|
+
newSet.delete(id)
|
|
68
|
+
return newSet
|
|
69
|
+
})
|
|
70
|
+
toast.success('Endpoint deleted successfully')
|
|
71
|
+
fetchEndpoints()
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('Failed to delete endpoint:', error)
|
|
74
|
+
toast.error('Failed to delete endpoint')
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const handleBulkDelete = async () => {
|
|
79
|
+
if (selectedIds.size === 0) return
|
|
80
|
+
if (!confirm(`Are you sure you want to delete ${selectedIds.size} endpoint(s)?`)) return
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetch('/api/endpoints/bulk', {
|
|
84
|
+
method: 'DELETE',
|
|
85
|
+
headers: { 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify({ ids: Array.from(selectedIds) }),
|
|
87
|
+
})
|
|
88
|
+
if (!response.ok) throw new Error('Failed to delete')
|
|
89
|
+
const count = selectedIds.size
|
|
90
|
+
setSelectedIds(new Set())
|
|
91
|
+
toast.success(`${count} endpoint(s) deleted successfully`)
|
|
92
|
+
fetchEndpoints()
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error('Failed to delete endpoints:', error)
|
|
95
|
+
toast.error('Failed to delete endpoints')
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const handleSave = async (endpoint: Partial<MockEndpoint> & { id?: string }) => {
|
|
100
|
+
try {
|
|
101
|
+
const isUpdate = !!endpoint.id
|
|
102
|
+
const response = isUpdate
|
|
103
|
+
? await fetch(`/api/endpoints/${endpoint.id}`, {
|
|
104
|
+
method: 'PUT',
|
|
105
|
+
headers: { 'Content-Type': 'application/json' },
|
|
106
|
+
body: JSON.stringify(endpoint),
|
|
107
|
+
})
|
|
108
|
+
: await fetch('/api/endpoints', {
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: { 'Content-Type': 'application/json' },
|
|
111
|
+
body: JSON.stringify(endpoint),
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
const data = await response.json()
|
|
116
|
+
throw new Error(data.error || 'Failed to save')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const savedEndpoint = await response.json()
|
|
120
|
+
setLastModifiedId(savedEndpoint.id)
|
|
121
|
+
toast.success(isUpdate ? 'Endpoint updated successfully' : 'Endpoint created successfully')
|
|
122
|
+
fetchEndpoints()
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error('Failed to save endpoint:', error)
|
|
125
|
+
toast.error(error instanceof Error ? error.message : 'Failed to save endpoint')
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div className="min-h-screen bg-gray-50">
|
|
131
|
+
<div className="max-w-7xl mx-auto py-8 px-4 sm:px-6 lg:px-8">
|
|
132
|
+
<div className="flex justify-between items-center mb-8">
|
|
133
|
+
<div>
|
|
134
|
+
<h1 className="text-3xl font-bold text-gray-900">Growthman</h1>
|
|
135
|
+
<p className="mt-1 text-gray-500">
|
|
136
|
+
Manage your mock API endpoints
|
|
137
|
+
</p>
|
|
138
|
+
{baseUrl && (
|
|
139
|
+
<p className="mt-2 text-sm font-mono bg-gray-100 px-3 py-1.5 rounded-md inline-block">
|
|
140
|
+
{baseUrl}/api/mock
|
|
141
|
+
</p>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
<div className="flex gap-3">
|
|
145
|
+
<Button variant="outline" onClick={() => setUploadDialogOpen(true)}>
|
|
146
|
+
Import OpenAPI
|
|
147
|
+
</Button>
|
|
148
|
+
<Button onClick={handleCreate}>
|
|
149
|
+
+ New Endpoint
|
|
150
|
+
</Button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
{selectedIds.size > 0 && (
|
|
155
|
+
<div className="mb-4 flex items-center gap-4 p-3 bg-blue-50 border border-blue-200 rounded-lg">
|
|
156
|
+
<span className="text-sm text-blue-800">
|
|
157
|
+
{selectedIds.size} endpoint(s) selected
|
|
158
|
+
</span>
|
|
159
|
+
<Button
|
|
160
|
+
variant="destructive"
|
|
161
|
+
size="sm"
|
|
162
|
+
onClick={handleBulkDelete}
|
|
163
|
+
>
|
|
164
|
+
Delete Selected
|
|
165
|
+
</Button>
|
|
166
|
+
<Button
|
|
167
|
+
variant="ghost"
|
|
168
|
+
size="sm"
|
|
169
|
+
onClick={() => setSelectedIds(new Set())}
|
|
170
|
+
>
|
|
171
|
+
Clear Selection
|
|
172
|
+
</Button>
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
|
|
176
|
+
<div className="bg-white shadow rounded-lg">
|
|
177
|
+
{loading ? (
|
|
178
|
+
<div className="p-8 text-center text-gray-500">Loading...</div>
|
|
179
|
+
) : (
|
|
180
|
+
<EndpointTable
|
|
181
|
+
endpoints={endpoints}
|
|
182
|
+
onEdit={handleEdit}
|
|
183
|
+
onDelete={handleDelete}
|
|
184
|
+
onTest={handleTest}
|
|
185
|
+
selectedIds={selectedIds}
|
|
186
|
+
onSelectionChange={setSelectedIds}
|
|
187
|
+
lastModifiedId={lastModifiedId}
|
|
188
|
+
/>
|
|
189
|
+
)}
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<EndpointEditDialog
|
|
194
|
+
endpoint={selectedEndpoint}
|
|
195
|
+
open={editDialogOpen}
|
|
196
|
+
onOpenChange={setEditDialogOpen}
|
|
197
|
+
onSave={handleSave}
|
|
198
|
+
/>
|
|
199
|
+
|
|
200
|
+
<OpenAPIUploadDialog
|
|
201
|
+
open={uploadDialogOpen}
|
|
202
|
+
onOpenChange={setUploadDialogOpen}
|
|
203
|
+
onImportComplete={fetchEndpoints}
|
|
204
|
+
/>
|
|
205
|
+
|
|
206
|
+
<ApiTestDialog
|
|
207
|
+
endpoint={testEndpoint}
|
|
208
|
+
open={testDialogOpen}
|
|
209
|
+
onOpenChange={setTestDialogOpen}
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
)
|
|
213
|
+
}
|
package/bin/cli.js
CHANGED
|
@@ -25,14 +25,12 @@ const env = {
|
|
|
25
25
|
// CLI 바이너리 경로 찾기
|
|
26
26
|
function getBinPath(packageName, binName) {
|
|
27
27
|
try {
|
|
28
|
-
// 패키지의 package.json 찾기
|
|
29
28
|
const pkgJsonPath = require.resolve(`${packageName}/package.json`, {
|
|
30
29
|
paths: [packageDir],
|
|
31
30
|
});
|
|
32
31
|
const pkgDir = path.dirname(pkgJsonPath);
|
|
33
32
|
const pkgJson = require(pkgJsonPath);
|
|
34
33
|
|
|
35
|
-
// bin 필드에서 실행 파일 경로 찾기
|
|
36
34
|
let binRelPath;
|
|
37
35
|
if (typeof pkgJson.bin === 'string') {
|
|
38
36
|
binRelPath = pkgJson.bin;
|
|
@@ -60,6 +58,18 @@ if (!fs.existsSync(dbPath)) {
|
|
|
60
58
|
});
|
|
61
59
|
}
|
|
62
60
|
|
|
61
|
+
// .next 폴더가 없으면 빌드
|
|
62
|
+
const nextDir = path.join(packageDir, '.next');
|
|
63
|
+
if (!fs.existsSync(nextDir)) {
|
|
64
|
+
console.log('Building application (first run)...');
|
|
65
|
+
const nextPath = getBinPath('next', 'next');
|
|
66
|
+
execSync(`node "${nextPath}" build`, {
|
|
67
|
+
cwd: packageDir,
|
|
68
|
+
stdio: 'inherit',
|
|
69
|
+
env,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
63
73
|
// 서버 시작
|
|
64
74
|
const port = process.env.PORT || 3100;
|
|
65
75
|
const url = `http://localhost:${port}`;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import dynamic from 'next/dynamic'
|
|
5
|
+
import { MockEndpoint } from '@/types/endpoint'
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
} from '@/components/ui/dialog'
|
|
12
|
+
import { Button } from '@/components/ui/button'
|
|
13
|
+
import { Input } from '@/components/ui/input'
|
|
14
|
+
import { Label } from '@/components/ui/label'
|
|
15
|
+
|
|
16
|
+
const MonacoEditor = dynamic(() => import('@monaco-editor/react'), {
|
|
17
|
+
ssr: false,
|
|
18
|
+
loading: () => <div className="h-[300px] bg-gray-100 animate-pulse rounded" />,
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
interface ApiTestDialogProps {
|
|
22
|
+
endpoint: MockEndpoint | null
|
|
23
|
+
open: boolean
|
|
24
|
+
onOpenChange: (open: boolean) => void
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const methodColors: Record<string, string> = {
|
|
28
|
+
GET: 'bg-green-100 text-green-800',
|
|
29
|
+
POST: 'bg-blue-100 text-blue-800',
|
|
30
|
+
PUT: 'bg-yellow-100 text-yellow-800',
|
|
31
|
+
PATCH: 'bg-orange-100 text-orange-800',
|
|
32
|
+
DELETE: 'bg-red-100 text-red-800',
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function ApiTestDialog({
|
|
36
|
+
endpoint,
|
|
37
|
+
open,
|
|
38
|
+
onOpenChange,
|
|
39
|
+
}: ApiTestDialogProps) {
|
|
40
|
+
const [loading, setLoading] = useState(false)
|
|
41
|
+
const [response, setResponse] = useState<{
|
|
42
|
+
status: number
|
|
43
|
+
statusText: string
|
|
44
|
+
body: string
|
|
45
|
+
time: number
|
|
46
|
+
} | null>(null)
|
|
47
|
+
const [error, setError] = useState<string | null>(null)
|
|
48
|
+
const [pathParams, setPathParams] = useState<Record<string, string>>({})
|
|
49
|
+
|
|
50
|
+
const baseUrl = typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000'
|
|
51
|
+
|
|
52
|
+
// Extract path parameters like :id, :userId from path
|
|
53
|
+
const extractPathParams = (path: string): string[] => {
|
|
54
|
+
const matches = path.match(/:(\w+)/g)
|
|
55
|
+
return matches ? matches.map((m) => m.slice(1)) : []
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const paramNames = endpoint ? extractPathParams(endpoint.path) : []
|
|
59
|
+
|
|
60
|
+
// Build the actual URL with path parameters replaced
|
|
61
|
+
const buildUrl = (): string => {
|
|
62
|
+
if (!endpoint) return ''
|
|
63
|
+
let path = endpoint.path
|
|
64
|
+
for (const [key, value] of Object.entries(pathParams)) {
|
|
65
|
+
path = path.replace(`:${key}`, value || `:${key}`)
|
|
66
|
+
}
|
|
67
|
+
return `${baseUrl}/api/mock${path}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const handleSend = async () => {
|
|
71
|
+
if (!endpoint) return
|
|
72
|
+
|
|
73
|
+
setLoading(true)
|
|
74
|
+
setError(null)
|
|
75
|
+
setResponse(null)
|
|
76
|
+
|
|
77
|
+
const startTime = performance.now()
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const res = await fetch(buildUrl(), {
|
|
81
|
+
method: endpoint.method,
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const endTime = performance.now()
|
|
85
|
+
const body = await res.text()
|
|
86
|
+
|
|
87
|
+
let formattedBody = body
|
|
88
|
+
try {
|
|
89
|
+
formattedBody = JSON.stringify(JSON.parse(body), null, 2)
|
|
90
|
+
} catch {
|
|
91
|
+
// Not JSON, use as-is
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
setResponse({
|
|
95
|
+
status: res.status,
|
|
96
|
+
statusText: res.statusText,
|
|
97
|
+
body: formattedBody,
|
|
98
|
+
time: Math.round(endTime - startTime),
|
|
99
|
+
})
|
|
100
|
+
} catch (err) {
|
|
101
|
+
setError(err instanceof Error ? err.message : 'Request failed')
|
|
102
|
+
} finally {
|
|
103
|
+
setLoading(false)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const handleOpenChange = (isOpen: boolean) => {
|
|
108
|
+
if (!isOpen) {
|
|
109
|
+
setResponse(null)
|
|
110
|
+
setError(null)
|
|
111
|
+
setPathParams({})
|
|
112
|
+
}
|
|
113
|
+
onOpenChange(isOpen)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const getStatusColor = (status: number) => {
|
|
117
|
+
if (status >= 200 && status < 300) return 'text-green-600'
|
|
118
|
+
if (status >= 400 && status < 500) return 'text-yellow-600'
|
|
119
|
+
if (status >= 500) return 'text-red-600'
|
|
120
|
+
return 'text-gray-600'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!endpoint) return null
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<Dialog open={open} onOpenChange={handleOpenChange}>
|
|
127
|
+
<DialogContent className="sm:max-w-[700px] max-h-[90vh] flex flex-col">
|
|
128
|
+
<DialogHeader>
|
|
129
|
+
<DialogTitle className="flex items-center gap-3">
|
|
130
|
+
<span
|
|
131
|
+
className={`px-2 py-1 rounded text-xs font-medium ${methodColors[endpoint.method] || 'bg-gray-100'}`}
|
|
132
|
+
>
|
|
133
|
+
{endpoint.method}
|
|
134
|
+
</span>
|
|
135
|
+
<span>Test API</span>
|
|
136
|
+
</DialogTitle>
|
|
137
|
+
</DialogHeader>
|
|
138
|
+
|
|
139
|
+
<div className="flex flex-col gap-4 flex-1 overflow-hidden">
|
|
140
|
+
{/* URL Display */}
|
|
141
|
+
<div className="flex items-center gap-2">
|
|
142
|
+
<Input
|
|
143
|
+
readOnly
|
|
144
|
+
value={buildUrl()}
|
|
145
|
+
className="flex-1 font-mono text-sm bg-gray-50"
|
|
146
|
+
/>
|
|
147
|
+
<Button onClick={handleSend} disabled={loading}>
|
|
148
|
+
{loading ? 'Sending...' : 'Send'}
|
|
149
|
+
</Button>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{/* Path Parameters */}
|
|
153
|
+
{paramNames.length > 0 && (
|
|
154
|
+
<div className="space-y-2 p-3 bg-gray-50 rounded-lg">
|
|
155
|
+
<Label className="text-sm font-medium">Path Parameters</Label>
|
|
156
|
+
<div className="grid gap-2">
|
|
157
|
+
{paramNames.map((param) => (
|
|
158
|
+
<div key={param} className="flex items-center gap-2">
|
|
159
|
+
<Label className="w-24 text-sm text-gray-600">:{param}</Label>
|
|
160
|
+
<Input
|
|
161
|
+
placeholder={`Enter ${param}`}
|
|
162
|
+
value={pathParams[param] || ''}
|
|
163
|
+
onChange={(e) =>
|
|
164
|
+
setPathParams((prev) => ({
|
|
165
|
+
...prev,
|
|
166
|
+
[param]: e.target.value,
|
|
167
|
+
}))
|
|
168
|
+
}
|
|
169
|
+
className="flex-1"
|
|
170
|
+
/>
|
|
171
|
+
</div>
|
|
172
|
+
))}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
|
|
177
|
+
{/* Error Display */}
|
|
178
|
+
{error && (
|
|
179
|
+
<div className="p-3 bg-red-50 border border-red-200 rounded-lg">
|
|
180
|
+
<p className="text-red-600 text-sm">{error}</p>
|
|
181
|
+
</div>
|
|
182
|
+
)}
|
|
183
|
+
|
|
184
|
+
{/* Response Display */}
|
|
185
|
+
{response && (
|
|
186
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
187
|
+
<div className="flex items-center gap-4 mb-2">
|
|
188
|
+
<span className="text-sm font-medium">Response</span>
|
|
189
|
+
<span className={`text-sm font-medium ${getStatusColor(response.status)}`}>
|
|
190
|
+
{response.status} {response.statusText}
|
|
191
|
+
</span>
|
|
192
|
+
<span className="text-sm text-gray-500">{response.time}ms</span>
|
|
193
|
+
</div>
|
|
194
|
+
<div className="border rounded-md overflow-hidden flex-1">
|
|
195
|
+
<MonacoEditor
|
|
196
|
+
height="300px"
|
|
197
|
+
language="json"
|
|
198
|
+
theme="vs-dark"
|
|
199
|
+
value={response.body}
|
|
200
|
+
options={{
|
|
201
|
+
readOnly: true,
|
|
202
|
+
minimap: { enabled: false },
|
|
203
|
+
fontSize: 13,
|
|
204
|
+
scrollBeyondLastLine: false,
|
|
205
|
+
automaticLayout: true,
|
|
206
|
+
}}
|
|
207
|
+
/>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
)}
|
|
211
|
+
|
|
212
|
+
{/* Initial State */}
|
|
213
|
+
{!response && !error && !loading && (
|
|
214
|
+
<div className="flex-1 flex items-center justify-center text-gray-400 border-2 border-dashed rounded-lg">
|
|
215
|
+
Click Send to test the API
|
|
216
|
+
</div>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
</DialogContent>
|
|
220
|
+
</Dialog>
|
|
221
|
+
)
|
|
222
|
+
}
|