commit d15899356e5d6386657eb519edf63dff4c3c3e4c Author: 0310700 Date: Mon Jun 15 15:11:50 2026 +0900 to-do first diff --git a/.bkit/audit/2026-06-15.jsonl b/.bkit/audit/2026-06-15.jsonl new file mode 100644 index 0000000..faa491a --- /dev/null +++ b/.bkit/audit/2026-06-15.jsonl @@ -0,0 +1,88 @@ +{"id":"17a935c7-dc43-4261-9ce5-a0fc7d05f38d","timestamp":"2026-06-15T05:46:12.725Z","sessionId":"","actor":"hook","actorId":"tool-failure-handler","action":"gate_failed","category":"control","target":"Bash","targetType":"feature","details":{"pattern":"exit_nonzero","severity":"low","error":"Exit code 2\n=== root ===\ntotal 148\ndrwxrwsr-x 6 coder coder 4096 6월 15 14:31 .\ndrwxrwsr-x 4 coder coder 4096 6월 15 13:58 ..\ndrwxr-sr-x 2 coder coder 4096 6월 15 14:44 .claude\n-rw-rw-r-- 1 coder coder 143 6월 15 13:58 .dockerignore\n-rw-rw-r-- 1 coder coder 166 6월 15 13:58 .gitignore\n-r","isInterrupt":false},"result":"failure","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"0295c2e7-4290-451c-974e-559c3d83c10d","timestamp":"2026-06-15T05:46:18.862Z","sessionId":"","actor":"system","actorId":"unified-bash-post","action":"command_executed","category":"control","target":"find /home/coder/.claude/plugins/cache/bkit-marketplace/bkit/2.1.22/skills/pdca -name \"*.template.md\" 2>/dev/null; echo \"=== templates dir ===\"; ls /home/coder/.claude/plugins/cache/bkit-marketplace/b","targetType":"feature","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"65d41428-4374-422a-854c-05593e72cc5c","timestamp":"2026-06-15T05:46:18.894Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Bash","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"1ffdefda-3782-4e20-abc2-f076e0bc4901","timestamp":"2026-06-15T05:46:53.838Z","sessionId":"","actor":"system","actorId":"notification-handler","action":"phase_transition","category":"control","target":"permission_prompt","targetType":"feature","details":{"event":"notification","notificationType":"permission_prompt","title":"","message":"Claude needs your permission"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"a535d914-4290-49d4-8231-3510f86893eb","timestamp":"2026-06-15T05:47:18.544Z","sessionId":"","actor":"system","actorId":"unified-bash-post","action":"command_executed","category":"control","target":"mkdir -p /home/coder/projects/to-do/docs/01-plan/features /home/coder/projects/to-do/.bkit/state 2>/dev/null; ls /home/coder/projects/to-do/.bkit 2>/dev/null; cat /home/coder/projects/to-do/.bkit/stat","targetType":"feature","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"d741b90f-bc2c-441c-b046-faca12ad7749","timestamp":"2026-06-15T05:47:18.579Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Bash","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"cc5c811d-f476-4f41-822a-485f9b8a317b","timestamp":"2026-06-15T05:48:12.457Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"destructive_blocked","category":"control","target":"/home/coder/projects/to-do/docs/01-plan/features/todo-app.plan.md","targetType":"file","details":{"rules":[{"id":"G-007","name":"Mass file deletion","severity":"medium","pattern":"\\b(rm|del|delete|remove)\\b.*(\\s+\\S+){5,}"}]},"result":"blocked","reason":null,"destructiveOperation":true,"blastRadius":"medium","bkitVersion":"2.1.22"} +{"id":"3f6e5234-238c-4b04-8a42-fcbbd11b7e80","timestamp":"2026-06-15T05:48:12.485Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/docs/01-plan/features/todo-app.plan.md","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"bbed62ab-87ce-4f37-a95c-87a5624acd00","timestamp":"2026-06-15T05:48:19.509Z","sessionId":"","actor":"system","actorId":"unified-bash-post","action":"command_executed","category":"control","target":"ls -la .bkit/state/ 2>/dev/null && echo \"=== existing status ===\" && cat .bkit/state/pdca-status.json 2>/dev/null; echo \"=== bkit-memory ===\" && cat .bkit-memory.json 2>/dev/null; echo \"---\"","targetType":"feature","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"11bd0c0d-8a49-4e0b-b528-9770f9a3c340","timestamp":"2026-06-15T05:48:19.543Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Bash","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"ad2855e2-0895-4186-b3f6-b4de5673a90d","timestamp":"2026-06-15T05:48:20.963Z","sessionId":"","actor":"hook","actorId":"task-created-handler","action":"task_created","category":"pdca","target":"[Plan] todo-app","targetType":"feature","details":{"taskId":"1","isPdcaTask":true,"pdcaPhase":"plan","pdcaFeature":"todo-app","isSprintTask":false,"sprintId":null,"sprintName":null},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"397d5bbb-1259-4aad-b7c0-4a65b5531844","timestamp":"2026-06-15T05:48:30.518Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/.bkit/state/pdca-status.json","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"322c89ef-ca51-4657-b725-3203b56b5567","timestamp":"2026-06-15T05:48:30.763Z","sessionId":"","actor":"system","actorId":"unified-write-post","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/.bkit/state/pdca-status.json","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"5c58231c-f402-489f-8805-4a5aecde235a","timestamp":"2026-06-15T05:48:30.810Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Write","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"394425dc-e003-422e-963b-8b719fe1c831","timestamp":"2026-06-15T05:50:42.029Z","sessionId":"","actor":"system","actorId":"notification-handler","action":"phase_transition","category":"control","target":"permission_prompt","targetType":"feature","details":{"event":"notification","notificationType":"permission_prompt","title":"","message":"Claude needs your permission"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"94a85839-ccbd-4ba7-99f0-ae0d4725f849","timestamp":"2026-06-15T05:50:59.807Z","sessionId":"","actor":"system","actorId":"unified-bash-post","action":"command_executed","category":"control","target":"mkdir -p /home/coder/projects/to-do/docs/02-design/features 2>/dev/null; echo done","targetType":"feature","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"f9bd966b-1930-45e5-a763-7b0832514150","timestamp":"2026-06-15T05:50:59.842Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Bash","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"0396eab8-0de8-4066-8df0-6800564ee78c","timestamp":"2026-06-15T05:52:02.913Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"destructive_blocked","category":"control","target":"/home/coder/projects/to-do/docs/02-design/features/todo-app.design.md","targetType":"file","details":{"rules":[{"id":"G-007","name":"Mass file deletion","severity":"medium","pattern":"\\b(rm|del|delete|remove)\\b.*(\\s+\\S+){5,}"}]},"result":"blocked","reason":null,"destructiveOperation":true,"blastRadius":"medium","bkitVersion":"2.1.22"} +{"id":"e27d0412-6b2f-4251-b3b1-726c7bfe1e0c","timestamp":"2026-06-15T05:52:02.938Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/docs/02-design/features/todo-app.design.md","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"80e73da2-4fea-482b-9e05-73db7e57d14c","timestamp":"2026-06-15T05:52:11.503Z","sessionId":"","actor":"hook","actorId":"task-created-handler","action":"task_created","category":"pdca","target":"[Design] todo-app","targetType":"feature","details":{"taskId":"2","isPdcaTask":true,"pdcaPhase":"design","pdcaFeature":"todo-app","isSprintTask":false,"sprintId":null,"sprintName":null},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"adcdaf9f-2793-4bb4-b2d2-6cef92adee2e","timestamp":"2026-06-15T05:52:29.394Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/.bkit/state/pdca-status.json","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"fb79cfe7-8514-4bc8-8eba-cdcf599c5bdb","timestamp":"2026-06-15T05:53:38.372Z","sessionId":"","actor":"hook","actorId":"task-created-handler","action":"task_created","category":"pdca","target":"[Do] todo-app","targetType":"feature","details":{"taskId":"3","isPdcaTask":true,"pdcaPhase":"do","pdcaFeature":"todo-app","isSprintTask":false,"sprintId":null,"sprintName":null},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"16a3c053-afc8-429e-b4c7-be428f413c69","timestamp":"2026-06-15T05:53:39.775Z","sessionId":"","actor":"hook","actorId":"task-created-handler","action":"task_created","category":"pdca","target":"module-1: 데이터 계층 (schema/s3/attachments/repo)","targetType":"feature","details":{"taskId":"4","isPdcaTask":false,"pdcaPhase":null,"pdcaFeature":null,"isSprintTask":false,"sprintId":null,"sprintName":null},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"031a2e19-e132-446b-bba6-2fc33f9f106c","timestamp":"2026-06-15T05:53:41.220Z","sessionId":"","actor":"hook","actorId":"task-created-handler","action":"task_created","category":"pdca","target":"module-2: API 계층 (routes/server/multer)","targetType":"feature","details":{"taskId":"5","isPdcaTask":false,"pdcaPhase":null,"pdcaFeature":null,"isSprintTask":false,"sprintId":null,"sprintName":null},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"66d345b1-4a44-44e8-a4c9-ef55b2748649","timestamp":"2026-06-15T05:53:42.213Z","sessionId":"","actor":"hook","actorId":"task-created-handler","action":"task_created","category":"pdca","target":"module-3: 웹 UI (public/index.html)","targetType":"feature","details":{"taskId":"6","isPdcaTask":false,"pdcaPhase":null,"pdcaFeature":null,"isSprintTask":false,"sprintId":null,"sprintName":null},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"002508b7-78cc-40fd-8c71-d63115001194","timestamp":"2026-06-15T05:53:57.600Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/src/schema.js","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"9adc2b82-5299-4f0f-ad04-d7fb40b1d642","timestamp":"2026-06-15T05:53:57.833Z","sessionId":"","actor":"system","actorId":"unified-write-post","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/src/schema.js","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"33ac8a69-067e-42a0-8f5b-37ff66a5d1ee","timestamp":"2026-06-15T05:53:57.869Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Write","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"0814b4f0-ba78-40d0-bad0-59ebf9402208","timestamp":"2026-06-15T05:54:13.499Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/src/s3.js","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"02b88d60-bed2-4ac7-a3c3-3a76dd5fd6ea","timestamp":"2026-06-15T05:54:21.929Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/src/s3.js","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"71d5904a-e299-4b01-ac95-87e60076a2ac","timestamp":"2026-06-15T05:54:31.777Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"destructive_blocked","category":"control","target":"/home/coder/projects/to-do/src/attachments.js","targetType":"file","details":{"rules":[{"id":"G-007","name":"Mass file deletion","severity":"medium","pattern":"\\b(rm|del|delete|remove)\\b.*(\\s+\\S+){5,}"}]},"result":"blocked","reason":null,"destructiveOperation":true,"blastRadius":"medium","bkitVersion":"2.1.22"} +{"id":"f41c045d-21f2-42b9-95ec-4318067dd151","timestamp":"2026-06-15T05:54:31.804Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/src/attachments.js","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"d600417c-cfc3-45b1-a68d-362140f3b267","timestamp":"2026-06-15T05:54:32.014Z","sessionId":"","actor":"system","actorId":"unified-write-post","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/src/attachments.js","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"5ec39bef-8f66-435e-bf35-df9485a53ca4","timestamp":"2026-06-15T05:54:32.049Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Write","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"65a10201-8397-4e44-a211-7b4563a6e083","timestamp":"2026-06-15T05:54:43.085Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"destructive_blocked","category":"control","target":"/home/coder/projects/to-do/src/todos.repo.js","targetType":"file","details":{"rules":[{"id":"G-007","name":"Mass file deletion","severity":"medium","pattern":"\\b(rm|del|delete|remove)\\b.*(\\s+\\S+){5,}"}]},"result":"blocked","reason":null,"destructiveOperation":true,"blastRadius":"medium","bkitVersion":"2.1.22"} +{"id":"c21d7fe6-a20e-4a72-95b7-59159b9189d0","timestamp":"2026-06-15T05:54:43.110Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/src/todos.repo.js","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"3325c75f-f574-4ea7-bf8c-47cac491c89c","timestamp":"2026-06-15T05:54:43.330Z","sessionId":"","actor":"system","actorId":"unified-write-post","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/src/todos.repo.js","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"2bdb4fc1-0653-499d-9855-59bb1f09379c","timestamp":"2026-06-15T05:54:43.370Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Write","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"6a35f288-9eac-46fa-8d80-f852f789c861","timestamp":"2026-06-15T05:55:11.892Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"destructive_blocked","category":"control","target":"/home/coder/projects/to-do/src/routes.todos.js","targetType":"file","details":{"rules":[{"id":"G-007","name":"Mass file deletion","severity":"medium","pattern":"\\b(rm|del|delete|remove)\\b.*(\\s+\\S+){5,}"}]},"result":"blocked","reason":null,"destructiveOperation":true,"blastRadius":"medium","bkitVersion":"2.1.22"} +{"id":"b69a9d37-a39b-4a57-8402-f1ffa197b0b4","timestamp":"2026-06-15T05:55:11.915Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/src/routes.todos.js","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"b13fbec8-bdac-49c3-a03f-6746846e80a4","timestamp":"2026-06-15T05:55:12.121Z","sessionId":"","actor":"system","actorId":"unified-write-post","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/src/routes.todos.js","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"919e032b-50c3-42cd-992a-8937f9e5c350","timestamp":"2026-06-15T05:55:12.167Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Write","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"fe0576bf-e634-4e4d-8f1a-8eb200d8e5f8","timestamp":"2026-06-15T05:55:19.368Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/src/todos.repo.js","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"a8b0a82d-eb9c-4596-a824-8897432ee2e1","timestamp":"2026-06-15T05:55:28.876Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/src/routes.todos.js","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"83da187b-b6c8-47b6-a48b-49d6d1d92a87","timestamp":"2026-06-15T05:55:35.419Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/src/routes.todos.js","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"989649e8-4791-45f4-b860-4e16e993ef5e","timestamp":"2026-06-15T05:55:47.375Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/src/routes.todos.js","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"ec8e86a9-1129-41ff-b32e-f33d67786d1a","timestamp":"2026-06-15T05:55:53.611Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/src/routes.todos.js","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"c62d2c47-4c77-4b52-93b4-5acf4890baa0","timestamp":"2026-06-15T05:56:07.005Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/src/server.js","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"2127598b-d6ea-4943-b21a-592123fecf3c","timestamp":"2026-06-15T05:56:07.246Z","sessionId":"","actor":"system","actorId":"unified-write-post","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/src/server.js","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"537ac447-1608-466c-8056-3868d34832a1","timestamp":"2026-06-15T05:56:07.279Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Write","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"8c4d6052-888c-47bb-a4f8-a4362e11a213","timestamp":"2026-06-15T05:56:13.060Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/package.json","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"ad263fc4-1d26-42e6-a22f-860fb8eedf95","timestamp":"2026-06-15T05:56:19.100Z","sessionId":"","actor":"system","actorId":"unified-bash-post","action":"command_executed","category":"control","target":"npm install 2>&1 | tail -15","targetType":"feature","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"e3f8db80-7d11-43c0-8c51-52a7674035a8","timestamp":"2026-06-15T05:56:19.133Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Bash","severity":"medium"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"af980ae6-574c-42bb-afd2-beb9a7b86e1b","timestamp":"2026-06-15T05:56:19.135Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_alarm_triggered","category":"system","target":"unknown","targetType":"feature","details":{"severity":"medium"},"result":"blocked","reason":"bkit ENH-289 Tier 2 alarm: medium severity detected on Bash","destructiveOperation":false,"blastRadius":"medium","bkitVersion":"2.1.22"} +{"id":"dcec2a14-6511-4d38-a681-d13963a4ac8a","timestamp":"2026-06-15T05:56:26.951Z","sessionId":"","actor":"system","actorId":"unified-bash-post","action":"command_executed","category":"control","target":"npm install multer@^2.0.0 2>&1 | tail -8 && echo \"=== installed version ===\" && node -e \"console.log(require('multer/package.json').version)\"","targetType":"feature","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"8c4d6a52-756e-46bf-beed-d5cb17f12d79","timestamp":"2026-06-15T05:56:26.983Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Bash","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"f7b7bb59-9fc8-4ac3-826a-264d7e4884c0","timestamp":"2026-06-15T05:56:54.482Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"destructive_blocked","category":"control","target":"/home/coder/projects/to-do/public/index.html","targetType":"file","details":{"rules":[{"id":"G-007","name":"Mass file deletion","severity":"medium","pattern":"\\b(rm|del|delete|remove)\\b.*(\\s+\\S+){5,}"}]},"result":"blocked","reason":null,"destructiveOperation":true,"blastRadius":"medium","bkitVersion":"2.1.22"} +{"id":"4d5cbf99-2ecb-46f8-bcba-0087e8da523e","timestamp":"2026-06-15T05:56:54.511Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/public/index.html","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"ee575244-f1df-4fe7-a9dc-d6d04bd96c01","timestamp":"2026-06-15T05:56:54.729Z","sessionId":"","actor":"system","actorId":"unified-write-post","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/public/index.html","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"506a4494-3347-43e9-a9c3-6500310302e4","timestamp":"2026-06-15T05:56:54.763Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Write","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"4e3ffb34-db21-4871-8abc-6df7df99293a","timestamp":"2026-06-15T05:57:00.326Z","sessionId":"","actor":"system","actorId":"unified-bash-post","action":"command_executed","category":"control","target":"echo \"=== syntax check all src ===\" && for f in src/schema.js src/s3.js src/attachments.js src/todos.repo.js src/routes.todos.js src/server.js; do node --check \"$f\" && echo \"OK $f\" || echo \"FAIL $f\"; ","targetType":"feature","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"4991dc30-d4f0-40a3-84ec-2fabf67ed8d5","timestamp":"2026-06-15T05:57:00.359Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Bash","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"99525ac8-6bd8-4706-9de6-c643d659eaf9","timestamp":"2026-06-15T05:57:09.066Z","sessionId":"","actor":"system","actorId":"unified-bash-post","action":"command_executed","category":"control","target":"echo \"=== db:check ===\" && timeout 30 npm run db:check 2>&1 | tail -8; echo \"=== minio:check ===\" && timeout 30 npm run minio:check 2>&1 | tail -8","targetType":"feature","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"2d6cbff0-ce78-43a8-9ccc-91b3888a52d1","timestamp":"2026-06-15T05:57:09.097Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Bash","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"29e24614-61db-4b90-9b63-9064bb61436a","timestamp":"2026-06-15T05:57:26.862Z","sessionId":"","actor":"system","actorId":"unified-bash-post","action":"command_executed","category":"control","target":"cd /home/coder/projects/to-do\n# 서버 백그라운드 기동\nnode src/server.js > /tmp/todo-server.log 2>&1 &\nSRV=$!\nsleep 2\necho \"=== server log ===\" && cat /tmp/todo-server.log\nBASE=http://localhost:3000\n\necho; echo","targetType":"feature","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"200a9a9d-3d02-4bc9-8243-8a428e1cbd21","timestamp":"2026-06-15T05:57:26.895Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Bash","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"82ffa5f2-c7e0-4eeb-a248-580bc71014ae","timestamp":"2026-06-15T05:57:40.746Z","sessionId":"","actor":"system","actorId":"unified-bash-post","action":"command_executed","category":"control","target":"cd /home/coder/projects/to-do\nnode src/server.js > /tmp/todo-server.log 2>&1 &\nSRV=$!\nsleep 2\n# 남은 테스트 todo 정리\nfor id in $(curl -s http://localhost:3000/api/todos | node -e \"let d='';process.stdin.on(","targetType":"feature","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"91ba2eea-c4af-4676-a5cd-e39e186a2821","timestamp":"2026-06-15T05:57:40.786Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Bash","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"f177a33c-b7aa-4cf1-b075-673e76138a35","timestamp":"2026-06-15T06:00:26.213Z","sessionId":"","actor":"hook","actorId":"permission-request-handler","action":"destructive_blocked","category":"control","target":"cd /home/coder/projects/to-do\nnode src/server.js > /tmp/todo-srv.log 2>&1 &\nSRV=$!; sleep 2\nBASE=http://localhost:3000\npass=0; fail=0\nchk(){ if [ \"$1\" = \"$2\" ]; then echo \" ✅ $3 ($1)\"; pass=$((pass+1","targetType":"feature","details":{"tool":"Bash","reason":"dangerous_command"},"result":"blocked","reason":"Dangerous bash command blocked by permission handler","destructiveOperation":true,"blastRadius":"critical","bkitVersion":"2.1.22"} +{"id":"cae37db8-78e1-4499-94f5-37d9eac4d74c","timestamp":"2026-06-15T06:00:31.739Z","sessionId":"","actor":"system","actorId":"unified-bash-post","action":"command_executed","category":"control","target":"cd /home/coder/projects/to-do\nnode src/server.js > /tmp/todo-srv.log 2>&1 &\nSRV=$!; sleep 2\nBASE=http://localhost:3000\npass=0; fail=0\nchk(){ if [ \"$1\" = \"$2\" ]; then echo \" ✅ $3 ($1)\"; pass=$((pass+1","targetType":"feature","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"42b4803c-b221-4c56-bf1b-273324cb5119","timestamp":"2026-06-15T06:00:31.773Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Bash","severity":"medium"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"ad9670f0-185b-408e-9cb1-77c4d868f5ca","timestamp":"2026-06-15T06:00:31.775Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_alarm_triggered","category":"system","target":"unknown","targetType":"feature","details":{"severity":"medium"},"result":"blocked","reason":"bkit ENH-289 Tier 2 alarm: medium severity detected on Bash","destructiveOperation":false,"blastRadius":"medium","bkitVersion":"2.1.22"} +{"id":"ee3f1176-f648-4924-917b-12a25db18139","timestamp":"2026-06-15T06:00:40.050Z","sessionId":"","actor":"hook","actorId":"permission-request-handler","action":"destructive_blocked","category":"control","target":"mkdir -p /home/coder/projects/to-do/docs/03-analysis 2>/dev/null; echo done","targetType":"feature","details":{"tool":"Bash","reason":"dangerous_command"},"result":"blocked","reason":"Dangerous bash command blocked by permission handler","destructiveOperation":true,"blastRadius":"critical","bkitVersion":"2.1.22"} +{"id":"617aed14-cf4f-4436-aa54-96756862f5cf","timestamp":"2026-06-15T06:00:43.774Z","sessionId":"","actor":"system","actorId":"unified-bash-post","action":"command_executed","category":"control","target":"mkdir -p /home/coder/projects/to-do/docs/03-analysis 2>/dev/null; echo done","targetType":"feature","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"30b39285-e714-4d58-b272-8bbd8dfea003","timestamp":"2026-06-15T06:00:43.807Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Bash","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"0c664265-3f2c-4f86-8aee-65f8b34ba814","timestamp":"2026-06-15T06:01:15.948Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"destructive_blocked","category":"control","target":"/home/coder/projects/to-do/docs/03-analysis/todo-app.analysis.md","targetType":"file","details":{"rules":[{"id":"G-007","name":"Mass file deletion","severity":"medium","pattern":"\\b(rm|del|delete|remove)\\b.*(\\s+\\S+){5,}"}]},"result":"blocked","reason":null,"destructiveOperation":true,"blastRadius":"medium","bkitVersion":"2.1.22"} +{"id":"809b924e-826f-4ed8-a4f3-cc59c6a538fc","timestamp":"2026-06-15T06:01:15.974Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/docs/03-analysis/todo-app.analysis.md","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"fc6ad0fb-9f87-4062-a2cb-39c8c08a1071","timestamp":"2026-06-15T06:01:16.177Z","sessionId":"","actor":"system","actorId":"unified-write-post","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/docs/03-analysis/todo-app.analysis.md","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"f090626f-b427-4501-8623-1023ec2468f4","timestamp":"2026-06-15T06:01:16.200Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Write","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"3b4038bf-5442-4bff-8aca-a03244a2fb97","timestamp":"2026-06-15T06:01:21.044Z","sessionId":"","actor":"hook","actorId":"task-created-handler","action":"task_created","category":"pdca","target":"[Check] todo-app","targetType":"feature","details":{"taskId":"7","isPdcaTask":true,"pdcaPhase":"check","pdcaFeature":"todo-app","isSprintTask":false,"sprintId":null,"sprintName":null},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"a269fac5-3642-4580-991b-3d9c73b0c21a","timestamp":"2026-06-15T06:02:02.890Z","sessionId":"","actor":"system","actorId":"unified-bash-post","action":"command_executed","category":"control","target":"mkdir -p /home/coder/projects/to-do/docs/04-report 2>/dev/null; echo done","targetType":"feature","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"af0e06ec-dc10-40cf-8648-844153cc1bdb","timestamp":"2026-06-15T06:02:02.921Z","sessionId":"","actor":"hook","actorId":"","action":"layer_6_audit_completed","category":"system","target":"unknown","targetType":"feature","details":{"tool":"Bash","severity":"low"},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"9b50ae2c-5288-4a0d-bd4d-d74d7dba841e","timestamp":"2026-06-15T06:02:12.105Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/.bkit/state/pdca-status.json","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"ec489c58-70ab-4b99-87e8-abb6e0569fe8","timestamp":"2026-06-15T06:02:42.645Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"destructive_blocked","category":"control","target":"/home/coder/projects/to-do/docs/04-report/todo-app.report.md","targetType":"file","details":{"rules":[{"id":"G-007","name":"Mass file deletion","severity":"medium","pattern":"\\b(rm|del|delete|remove)\\b.*(\\s+\\S+){5,}"}]},"result":"blocked","reason":null,"destructiveOperation":true,"blastRadius":"medium","bkitVersion":"2.1.22"} +{"id":"3e43b7c5-bcbd-4db1-9ae4-2782f7e899e5","timestamp":"2026-06-15T06:02:42.672Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/docs/04-report/todo-app.report.md","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"3a421830-1b52-4d09-8594-8cae5a370eb4","timestamp":"2026-06-15T06:02:47.786Z","sessionId":"","actor":"hook","actorId":"task-created-handler","action":"task_created","category":"pdca","target":"[Report] todo-app","targetType":"feature","details":{"taskId":"8","isPdcaTask":true,"pdcaPhase":"report","pdcaFeature":"todo-app","isSprintTask":false,"sprintId":null,"sprintName":null},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"88dd1c40-4ed6-4923-8120-91d01f704215","timestamp":"2026-06-15T06:02:54.199Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/.bkit/state/pdca-status.json","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} +{"id":"c59f0f92-c2eb-427d-872e-59f8ae973edc","timestamp":"2026-06-15T06:02:55.914Z","sessionId":"","actor":"hook","actorId":"pre-write","action":"file_modified","category":"file","target":"/home/coder/projects/to-do/.bkit/state/pdca-status.json","targetType":"file","details":{},"result":"success","reason":null,"destructiveOperation":false,"blastRadius":null,"bkitVersion":"2.1.22"} diff --git a/.bkit/runtime/agent-state.json b/.bkit/runtime/agent-state.json new file mode 100644 index 0000000..5354279 --- /dev/null +++ b/.bkit/runtime/agent-state.json @@ -0,0 +1,21 @@ +{ + "version": "1.0", + "enabled": false, + "teamName": "", + "feature": "todo-app", + "pdcaPhase": "do", + "orchestrationPattern": "leader", + "ctoAgent": "opus", + "startedAt": "2026-06-15T05:58:28.486Z", + "lastUpdated": "2026-06-15T06:06:20.059Z", + "teammates": [], + "progress": { + "totalTasks": 0, + "completedTasks": 0, + "inProgressTasks": 0, + "failedTasks": 0, + "pendingTasks": 0 + }, + "recentMessages": [], + "sessionId": "fc2739d9-56c6-41c1-ba6a-4640921018a2" +} \ No newline at end of file diff --git a/.bkit/runtime/cc-event-log.ndjson b/.bkit/runtime/cc-event-log.ndjson new file mode 100644 index 0000000..f29ed89 --- /dev/null +++ b/.bkit/runtime/cc-event-log.ndjson @@ -0,0 +1,6 @@ +{"hookEvent":"Stop","ccVersion":"2.1.177","sessionId":"fc2739d9-56c6-41c1-ba6a-4640921018a2","timestamp":"2026-06-15T05:48:44.074Z","context":{"agent":"main","skill":null}} +{"hookEvent":"Stop","ccVersion":"2.1.177","sessionId":"fc2739d9-56c6-41c1-ba6a-4640921018a2","timestamp":"2026-06-15T05:52:43.691Z","context":{"agent":"main","skill":null}} +{"hookEvent":"Stop","ccVersion":"2.1.177","sessionId":"fc2739d9-56c6-41c1-ba6a-4640921018a2","timestamp":"2026-06-15T05:58:02.379Z","context":{"agent":"main","skill":null}} +{"hookEvent":"SubagentStop","ccVersion":"2.1.177","sessionId":"fc2739d9-56c6-41c1-ba6a-4640921018a2","timestamp":"2026-06-15T06:00:01.000Z","context":{"agentType":"bkit:gap-detector","status":"completed"}} +{"hookEvent":"Stop","ccVersion":"2.1.177","sessionId":"fc2739d9-56c6-41c1-ba6a-4640921018a2","timestamp":"2026-06-15T06:03:15.859Z","context":{"agent":"main","skill":null}} +{"hookEvent":"SubagentStop","ccVersion":"2.1.177","sessionId":"fc2739d9-56c6-41c1-ba6a-4640921018a2","timestamp":"2026-06-15T06:06:20.086Z","context":{"agentType":"unknown","status":"completed"}} diff --git a/.bkit/runtime/hook-reachability.json b/.bkit/runtime/hook-reachability.json new file mode 100644 index 0000000..59af1c9 --- /dev/null +++ b/.bkit/runtime/hook-reachability.json @@ -0,0 +1,10 @@ +{ + "bash_post": { + "ts": "2026-06-15T06:02:02.922Z", + "version": "2.1.15" + }, + "write_post": { + "ts": "2026-06-15T06:01:16.201Z", + "version": "2.1.14" + } +} \ No newline at end of file diff --git a/.bkit/runtime/session-title-cache.json b/.bkit/runtime/session-title-cache.json new file mode 100644 index 0000000..bd2eae7 --- /dev/null +++ b/.bkit/runtime/session-title-cache.json @@ -0,0 +1,11 @@ +{ + "$schemaVersion": 2, + "sessions": { + "fc2739d9-56c6-41c1-ba6a-4640921018a2": { + "feature": "todo-app", + "phase": null, + "action": null, + "timestamp": "2026-06-15T05:52:57.044Z" + } + } +} \ No newline at end of file diff --git a/.bkit/runtime/token-ledger.ndjson b/.bkit/runtime/token-ledger.ndjson new file mode 100644 index 0000000..c1223c8 --- /dev/null +++ b/.bkit/runtime/token-ledger.ndjson @@ -0,0 +1,4 @@ +{"ts":"2026-06-15T05:48:44.071Z","sessionHash":"acac19c07c52072a","agent":"main","model":"unknown","ccVersion":"2.1.177","turnIndex":0,"inputTokens":0,"outputTokens":0,"cacheReadInputTokens":0,"cacheCreationInputTokens":0,"overheadDelta":0,"parseStatus":"no_payload","parseWarnings":"no message field in hookContext (env-fallback)"} +{"ts":"2026-06-15T05:52:43.688Z","sessionHash":"acac19c07c52072a","agent":"main","model":"unknown","ccVersion":"2.1.177","turnIndex":0,"inputTokens":0,"outputTokens":0,"cacheReadInputTokens":0,"cacheCreationInputTokens":0,"overheadDelta":0,"parseStatus":"no_payload","parseWarnings":"no message field in hookContext (env-fallback)"} +{"ts":"2026-06-15T05:58:02.377Z","sessionHash":"acac19c07c52072a","agent":"main","model":"unknown","ccVersion":"2.1.177","turnIndex":0,"inputTokens":0,"outputTokens":0,"cacheReadInputTokens":0,"cacheCreationInputTokens":0,"overheadDelta":0,"parseStatus":"no_payload","parseWarnings":"no message field in hookContext (env-fallback)"} +{"ts":"2026-06-15T06:03:15.857Z","sessionHash":"acac19c07c52072a","agent":"main","model":"unknown","ccVersion":"2.1.177","turnIndex":0,"inputTokens":0,"outputTokens":0,"cacheReadInputTokens":0,"cacheCreationInputTokens":0,"overheadDelta":0,"parseStatus":"no_payload","parseWarnings":"no message field in hookContext (env-fallback)"} diff --git a/.bkit/state/pdca-status.json b/.bkit/state/pdca-status.json new file mode 100644 index 0000000..4b8da6d --- /dev/null +++ b/.bkit/state/pdca-status.json @@ -0,0 +1,120 @@ +{ + "version": "3.0", + "lastUpdated": "2026-06-15T06:02:47.976Z", + "activeFeatures": [ + "todo-app" + ], + "primaryFeature": "todo-app", + "features": { + "todo-app": { + "phase": "completed", + "startedAt": "2026-06-15T00:00:00Z", + "updatedAt": "2026-06-15T00:00:00Z", + "documents": { + "plan": "docs/01-plan/features/todo-app.plan.md", + "design": "docs/02-design/features/todo-app.design.md", + "analysis": "docs/03-analysis/todo-app.analysis.md", + "report": "docs/04-report/todo-app.report.md" + }, + "matchRate": 99, + "iterationCount": 0, + "requirements": [], + "timestamps": { + "started": "2026-06-15T00:00:00Z", + "lastUpdated": "2026-06-15T06:02:47.974Z" + }, + "stateMachine": { + "currentState": "plan", + "previousState": null, + "stateHistory": [], + "retryCount": 0, + "maxRetries": 5, + "circuitBreakerOpen": false + }, + "metrics": { + "qualityScore": null, + "conventionCompliance": null, + "apiCompliance": null, + "cycleTimeMs": null, + "iterationEfficiency": null + }, + "phaseTimestamps": {}, + "automationLevel": 2, + "phaseNumber": 5, + "previousPhase": "check", + "autoAdvanced": true + } + }, + "pipeline": { + "currentPhase": 1, + "level": "Dynamic", + "phaseHistory": [] + }, + "session": { + "startedAt": "2026-06-15T05:48:30.802Z", + "onboardingCompleted": false, + "lastActivity": "2026-06-15T06:02:47.976Z" + }, + "history": [ + { + "timestamp": "2026-06-15T05:52:18.433Z", + "feature": "todo-app", + "phase": "design", + "action": "updated" + }, + { + "timestamp": "2026-06-15T05:57:45.693Z", + "feature": "todo-app", + "phase": "do", + "action": "updated" + }, + { + "timestamp": "2026-06-15T06:02:47.976Z", + "feature": "todo-app", + "phase": "act", + "action": "updated" + } + ], + "stateMachine": { + "defaultWorkflow": "default", + "activeWorkflows": {}, + "totalTransitions": 0 + }, + "automation": { + "globalLevel": 2, + "trustScore": 38, + "pendingApprovals": 0, + "lastGateResult": null + }, + "team": { + "enabled": true, + "stateFile": ".bkit/runtime/agent-state.json", + "eventsFile": ".bkit/runtime/agent-events.jsonl" + }, + "taskHistory": [ + { + "taskId": "2", + "phase": "design", + "feature": "todo-app", + "createdAt": "2026-06-15T05:52:11.526Z" + }, + { + "taskId": "3", + "phase": "do", + "feature": "todo-app", + "createdAt": "2026-06-15T05:53:38.395Z" + }, + { + "taskId": "7", + "phase": "check", + "feature": "todo-app", + "createdAt": "2026-06-15T06:01:21.069Z" + }, + { + "taskId": "8", + "phase": "report", + "feature": "todo-app", + "createdAt": "2026-06-15T06:02:47.809Z" + } + ] +} \ No newline at end of file diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..bc78583 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,6 @@ +{ + "enabledPlugins": { + "bkit-starter@bkit-marketplace": true, + "bkit@bkit-marketplace": true + } +} diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..145c7c3 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,8 @@ +{ + "permissions": { + "allow": [ + "Bash(echo \"=== ~/.claude/settings.json ===\"; cat ~/.claude/settings.json 2>/dev/null || echo \"\\(missing\\)\"; echo; echo \"=== .claude/settings.json ===\"; cat /home/coder/projects/to-do/.claude/settings.json 2>/dev/null || echo \"\\(missing\\)\"; echo; echo \"=== .claude/settings.local.json ===\"; cat /home/coder/projects/to-do/.claude/settings.local.json 2>/dev/null || echo \"\\(missing\\)\")", + "Read(//home/coder/.claude/**)" + ] + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a27bd43 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +node_modules +npm-debug.log +.project-env +.project-env.example +.env +.env.* +.git +.gitignore +Dockerfile +compose.yaml +README.md +CLAUDE.md +MANUAL.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9785f64 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +# 프로젝트 설정(DB/S3 자격증명 포함) — 절대 커밋 금지 +.project-env +.env +.env.* +!.project-env.example +npm-debug.log* +dist/ +.DS_Store diff --git a/.project-env.example b/.project-env.example new file mode 100644 index 0000000..5e53b93 --- /dev/null +++ b/.project-env.example @@ -0,0 +1,29 @@ +# .project-env.example — 프로젝트 설정 예시(참조용). +# +# 실제 사용: `new-project <이름>` 명령으로 sample을 복사하면 .project-env 가 +# DB(본인 schema 자동계산)+S3 값이 채워진 채로 자동 생성됩니다. +# 이 example 을 직접 복사해 쓸 일은 거의 없습니다(수동 생성 시 참고용). +# +# 동작: 프로젝트 폴더에 cd 하면 로그인 셸이 .project-env 를 자동 export → 앱은 +# process.env 로 바로 읽습니다(별도 dotenv 불필요). git에는 올리지 마세요(.gitignore). + +# --- DB (본인 전용 Postgres schema) --- +# host 10.200.0.152 는 워크스페이스/Coolify 배포 양쪽에서 동일하게 도달(그대로 복붙 가능). +DATABASE_URL=postgresql://emp_<사번>:@10.200.0.152:5432/appdb?options=-c%20search_path%3Demp_<사번> +PGHOST=10.200.0.152 +PGPORT=5432 +PGDATABASE=appdb +PGUSER=emp_<사번> +PGPASSWORD= +PGOPTIONS=-c search_path=emp_<사번> + +# --- MinIO / S3 --- +# endpoint 는 콘솔(minioc)이 아니라 API 호스트(minio)입니다. +S3_ENDPOINT=https://minio.bokdev.in +S3_REGION=us-east-1 +S3_BUCKET=coolify-user-data +S3_ACCESS_KEY_ID=REPLACE_WITH_YOUR_MINIO_ACCESS_KEY +S3_SECRET_ACCESS_KEY=REPLACE_WITH_YOUR_MINIO_SECRET_KEY + +# --- 앱 --- +PORT=3000 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6bcb2f6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,42 @@ +# CLAUDE.md — 이 프로젝트에서 Claude Code가 따라야 할 규약 + +너는 사내 개발 워크스페이스에서 동작하는 코딩 에이전트다. 이 프로젝트는 **Node(ESM) 앱**이고, +**로컬 개발/테스트는 podman**, **배포는 Coolify(Gitea push → Dockerfile 자동빌드)** 로 한다. + +## 절대 규칙 +- **비밀값을 코드/커밋에 넣지 마라.** DB·MinIO 자격증명은 항상 환경변수에서만 읽는다. + 값은 프로젝트의 `.project-env` 에 있고(프로젝트 폴더 cd 시 셸이 자동 export), 이 파일은 **절대 커밋 금지**(`.gitignore`에 있음). 참고용 예시는 `.project-env.example`. +- 컨테이너는 `docker` 명령으로 실행해도 되지만 **실제 런타임은 rootless podman**이다(`docker`는 podman 별칭). +- 운영 배포를 직접 하지 마라. 배포는 **git push → Coolify**가 담당한다. 너는 코드/Dockerfile만 책임진다. + +## 외부 연결 (프로젝트 `.project-env` 에서 env로 주입됨) +- **Postgres**: `DATABASE_URL` (직원 전용 schema로 격리, `search_path` 고정). 코드는 `src/db.js`를 통해 접근. +- **MinIO(S3)**: API endpoint `https://minio.bokdev.in` (콘솔 `minioc`가 아님), 버킷 `coolify-user-data`. + 키는 `.project-env`의 `S3_ACCESS_KEY_ID`/`S3_SECRET_ACCESS_KEY`. 코드는 `src/s3.js`를 통해 접근. +- **LiteLLM**: AI 호출은 사내 gateway(`https://litellm.bok.or.kr`) 경유. virtual key는 워크스페이스 전역인 `~/.env`(`LITELLM_KEY`)에서 관리 — `update-litellm-key` 명령으로 입력. (프로젝트 설정 아님) + +## 로컬 개발 워크플로 (podman) +```bash +# 이 폴더는 new-project 로 만들어졌고 .project-env(DB/S3)가 이미 채워져 있다. +# 폴더에 들어와 있으면 셸이 .project-env 를 자동 export 한 상태다($DATABASE_URL 등 사용 가능). +npm install +npm run dev # node --watch 로 핫리로드, http://localhost:3000 + +# 컨테이너로 동일하게 돌려보기 +podman build -t sample . +podman run --rm -p 3000:3000 --env-file .project-env sample +# 또는 +podman compose up --build +``` +연결 확인: `npm run db:check`, `npm run minio:check`, 또는 `/db` `/s3` 엔드포인트. + +## 배포 워크플로 (Coolify) +1. Gitea repo에 push (`main` 등 배포 브랜치). +2. Coolify가 이 **Dockerfile**로 자동 빌드/배포한다(로컬 podman 빌드와 동일 Dockerfile). +3. Coolify 쪽 Environment Variables 에 `DATABASE_URL`, `S3_*` 를 채운다(로컬 `.project-env`와 같은 키). +4. 포트는 컨테이너 `3000`. Coolify에서 도메인/포트 매핑. + +## 코드 규약 +- ESM(`import`/`export`), Node 22+. 외부 연결정보는 반드시 `src/config.js` 한 곳을 거친다. +- 새 외부 의존성을 추가하면 `package.json`에 반영하고 `npm install`로 lock 갱신. +- 변경 후에는 `npm run db:check` / `npm run minio:check`로 연결이 살아있는지 스스로 확인하라. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cf7621d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# 로컬(podman)과 Coolify 자동빌드가 공유하는 단일 Dockerfile. +# - 로컬: podman build -t sample . && podman run --rm -p 3000:3000 --env-file .project-env sample +# - 배포: Gitea push → Coolify가 이 Dockerfile로 빌드/배포 +FROM node:22-alpine + +WORKDIR /app + +# 의존성 레이어 캐시 +COPY package.json package-lock.json* ./ +RUN npm ci --omit=dev || npm install --omit=dev + +# 앱 소스 +COPY . . + +ENV NODE_ENV=production \ + PORT=3000 +EXPOSE 3000 + +# 비루트 실행 +USER node + +CMD ["node", "src/server.js"] diff --git a/MANUAL.md b/MANUAL.md new file mode 100644 index 0000000..633ddb0 --- /dev/null +++ b/MANUAL.md @@ -0,0 +1,424 @@ +# 개발환경 사용 매뉴얼 (Coder) + +사내 개발은 **Coder**(웹 기반 개발 워크스페이스)에서 합니다. +브라우저만 있으면 됩니다. +이 개발환경에는 DB, MinIO, Coding Agent, git, 배포환경이 미리 연결돼 있어, 로그인 후 바로 이용할 수 있습니다. + +--- + +## 0. 준비물 +- 인터넷 접속 가능한 브라우저 (Chrome/Edge 권장) + +### 주소(URL) 한눈에 + +| 용도 | 주소 | 쓰는 곳 | +|---|---|---| +| **개발 워크스페이스** | https://coder.bokdev.in | 로그인·VS Code (1~3번) | +| **코드 저장소(Git)** | https://gitea.bokdev.in | repo 생성·push (11번) | +| **배포(Coolify)** | https://coolify.bokdev.in | 앱 빌드·배포·로그 (12번) | +| **파일저장소(MinIO 콘솔)** | https://minioc.bokdev.in | 업로드된 파일 눈으로 확인 (선택) | +| **개발 중 앱 미리보기** | `https://<자동생성>.coder.bokdev.in` | 로컬 실행 앱 브라우저 확인 (9·10번, VS Code PORTS가 자동 발급) | +| **배포된 앱 주소** | `https://<이름>.apps.bokdev.in` | 실제 서비스 확인 (12번, Coolify에서 도메인 지정) | + +> 코드에서 쓰는 주소(자동 주입, 직접 입력 불필요): **DB** `10.200.0.152:5432` · **MinIO API** `https://minio.bokdev.in` · **AI 게이트웨이** `https://litellm.bok.or.kr`. +> (콘솔 `minioc` 와 API `minio` 는 다릅니다 — 코드는 `minio`, 사람이 눈으로 볼 땐 `minioc`.) + +--- + +## 1. Coder 로그인 + +1. 브라우저에서 **https://coder.bokdev.in** 접속 +2. 로그인 + - **Username**: 본인 행번 (예: `2610700`) + - **Password**: `bok1234!!` + 본인 사번 (예: `bok1234!!2610700`) + - > 최초 로그인 후 비밀번호를 바꾸시기 바랍니다. (우측 상단 계정 메뉴 → Account) + +--- + +## 2. 워크스페이스 만들기 (최초 1회) + +워크스페이스 = 본인 전용 개발용 컨테이너(VS Code + 각종 도구가 깔린 PC). + +1. 로그인하면 **Workspaces** 화면. **`Create Workspace`** 클릭 (또는 **Templates → `aidev`** 선택) +2. 설정값 입력 + - **Name**: 워크스페이스 이름 (예: `ws-aidev-<사번>` 또는 자유롭게) + - **External Authentication** : Gitea(그대로 둠) + - **CPU / Memory / Home disk size**: 기본값(2 Core / 4 GiB / 10 GiB)으로 두면 됩니다. 필요하면 나중에 늘릴 수 있습니다. +3. **`Create Workspace`** 클릭 +4. 워크스페이스가 빌드됩니다 (**처음엔 이미지 다운로드로 2~5분** 걸릴 수 있습니다. 오래 걸리거나 기대한 시간보다 오래 걸린다는 경고 메시지가 나와도 VS Code Web 까지 아이콘이 표시될 때까지 기다리세요). + - 상태가 **Running** 이 되면 준비 완료. + +> 이 워크스페이스는 한번 만들면 계속 사용합니다. 한번 만들어진 워크스페이스는 가상 PC처럼 **Start** 만 누르면 됩니다. + +--- + +## 3. VS Code 열기 + +1. 워크스페이스 화면에서 **`VS Code Web`** 버튼(두번째 아이콘) 클릭 +2. 브라우저에 VS Code가 열리고, 자동으로 **`/home/coder/projects`** 폴더가 열립니다. +3. 그 안에 **`sample`** 폴더가 이미 있습니다 — 참조용 예제입니다(직접 고치지 말고 복사해 쓰세요, 7번 참고). + +> 작업 파일은 반드시 **`/home/coder/projects`** 아래에 두세요. 이 폴더만 영구 보존됩니다. +> (워크스페이스를 stop/재시작해도 유지. 그 밖의 위치는 사라질 수 있습니다.) + +--- + +## 4. 미리 연결된 것들 (별도 설정 불필요) + +새 워크스페이스에는 아래가 자동으로 준비돼 있습니다. + +| 항목 | 내용 | +|---|---| +| **개발 도구** | Java(JDK)/Maven, Node 22, Python 3.12, git, psql, `tree`, `net-tools`(netstat/ifconfig) | +| **컨테이너** | `podman` (그리고 `docker` 명령도 동일하게 동작 — podman 별칭) | +| **DB** | 본인 전용 Postgres 스키마에 자동 연결 (`$DATABASE_URL`) | +| **VS Code 확장** | Claude Code, Codex (이미 설치됨) | +| **AI CLI** | `claude`, `codex`, `gemini` (LiteLLM 게이트웨이 연동, 아래 5번 참고) | + +--- + +## 5. AI 에이전트 사용 설정 (최초 1회) — LiteLLM 키 입력 + +Claude Code / Codex / Gemini 가 사내 AI 게이트웨이를 쓰려면 **본인 LiteLLM virtual key**가 필요합니다. +키는 **한 곳에만** 넣으면 모든 도구(CLI·확장)가 공유합니다. + +1. 터미널 열기: VS Code 상단 메뉴 **Terminal → New Terminal** +2. 아래 명령을 입력하고, 안내가 나오면 발급받은 본인 키(`sk-...`)를 붙여넣습니다: + ```bash + update-litellm-key + ``` + ``` + LiteLLM virtual key 입력 (sk-...): sk-여기에-본인-키-붙여넣기 + 키 갱신 완료 (len=25). 현재 터미널에 즉시 적용됨. + ``` + > `update-litellm-key` 는 키를 파일에 저장하고 **현재 터미널에 바로 적용**합니다. + > 터미널을 새로 열거나 `source` 할 필요가 없습니다. 키를 바꿀 때도 같은 명령을 다시 쓰면 됩니다. +3. 확인: + ```bash + echo $ANTHROPIC_BASE_URL # https://litellm.bok.or.kr 가 나오면 정상 + claude # Claude Code CLI 실행 + ``` + +각 도구의 **기본 모델**은 사내 게이트웨이에 맞춰 미리 설정돼 있습니다(바꾸려면 각 설정 파일 수정): + +| 도구 | 기본 모델 | 설정 파일 | +|---|---|---| +| Claude Code | `claude-opus-4-8` | `~/.claude/settings.json` | +| Codex | `gpt-5.5` | `~/.codex/config.toml` | +| Gemini | `gemini-3.1-pro-preview` | `~/.gemini/settings.json` | + +> 키는 본인 워크스페이스 안에만 저장되고, 코드/깃에는 올라가지 않습니다. +> 게이트웨이 주소(`https://litellm.bok.or.kr`)는 이미 설정돼 있으니 건드릴 필요 없습니다. + +--- + +## 6. Git(Gitea) 사용 — 최초 1회 승인 + +코드 저장소는 사내 **Gitea**(https://gitea.bokdev.in)입니다. 토큰 입력 없이 자동 인증됩니다. + +1. 터미널에서 처음 `git clone` / `git push` 등을 하면, **Gitea 승인 화면**으로 안내됩니다. + - 또는 Coder 화면의 **`Gitea`** external auth 항목에서 **Authorize** 를 미리 눌러도 됩니다. +2. 한 번 **Authorize(승인)** 하면, 이후로는 비밀번호 입력 없이 clone/push 가 됩니다. + +예: +```bash +cd ~/projects +git clone https://gitea.bokdev.in//.git +``` + +--- + +## 7. 새 프로젝트 시작하기 (sample 복사) + +`~/projects/sample` 은 바로 돌려볼 수 있는 Node 예제이며 **읽기 전용 참조**입니다. +직접 고치지 말고, **`new-project` 명령으로 복사**해서 본인 프로젝트를 시작하세요. + +**(1) 터미널 열기** — VS Code 상단 메뉴 **Terminal → New Terminal** (화면 아래쪽에 터미널 창이 뜹니다) + +**(2) 프로젝트 만들기** — 터미널에 입력: +```bash +new-project myapp # sample 을 ~/projects/myapp 으로 복사 + .project-env(DB/S3) 자동생성 +``` +> `myapp` 은 예시입니다. 원하는 이름으로 바꿔도 됩니다. + +**(3) VS Code 로 그 폴더 열기** — 만든 폴더를 편집기에 띄웁니다(왼쪽 파일 목록에 보이게): + - 상단 메뉴 **File → Open Folder…** + - 경로 입력칸에 **`/home/coder/projects/myapp`** 입력 → **OK** + - (또는 왼쪽 맨 위 📁 **Explorer** 아이콘 → **Open Folder** 버튼) + - 창이 새로고침되며 왼쪽에 `myapp` 의 파일들이 보이면 성공입니다. + +> 폴더를 열면 VS Code 가 그 폴더를 "작업 공간"으로 삼습니다. 이제 왼쪽 목록에서 파일을 눌러 편집하고, +> 터미널도 자동으로 그 폴더(`~/projects/myapp`)에서 시작됩니다. + +**(4) 라이브러리 설치** — 다시 터미널(Terminal → New Terminal)에서: +```bash +npm install # 처음 1회: 필요한 라이브러리 설치 (이걸 안 하면 실행이 실패합니다) +``` + +여기까지 하면 프로젝트 준비 완료입니다. **이제 8번부터 본격적으로 개발**하면 됩니다(코드 편집 → AI 활용 → 실행 → 배포). + +> **`.project-env` 가 그 프로젝트의 설정 파일입니다** (DB·S3). 값을 바꾸려면 이 파일 한 곳만 고치면 됩니다. +> 프로젝트 폴더에 들어가면(cd) 자동으로 환경변수에 반영됩니다. (git에는 올라가지 않습니다) +> LiteLLM 키만은 프로젝트가 아니라 워크스페이스 전체 공용이라 `~/.env`(5번 `update-litellm-key`)에서 관리합니다. + +--- + +## 📌 전체 개발 흐름 (한눈에) + +``` +7. 새 프로젝트 만들기 (new-project → 폴더 열기 → 설치) ← 위에서 완료 +8. 개발하기 (코드 편집 + AI + DB/S3 사용) +9. 로컬에서 실행·확인 (npm run dev → 브라우저 미리보기) +10. 컨테이너로 빌드·확인 (podman build/run — 배포와 동일한 방식으로 검증) +11. Gitea에 올리기 (git push) +12. Coolify로 배포·확인 (실제 서비스로 띄우기) +``` +아래는 각 단계를 순서대로 설명합니다. + +--- + +## 8. 개발하기 + +(7번에서 **File → Open Folder 로 본인 프로젝트 폴더를 열어 둔 상태**에서) + +- **코드 편집**: 왼쪽 파일 목록에서 파일(예: `src/server.js`)을 눌러 수정 → **Ctrl+S 로 저장**. +- **AI 도구 활용**: 터미널에서 `claude`(또는 `codex`/`gemini`) 실행. **반드시 본인 프로젝트 폴더 안에서** 실행해야 그 프로젝트를 봅니다(폴더를 Open Folder 해뒀으면 새 터미널은 이미 그 폴더에서 시작). 키 설정은 5번. +- **내 DB 직접 접속**(필요 시): + ```bash + cd ~/projects/<본인 프로젝트> # 프로젝트 폴더에서 (그래야 .project-env 가 로드됨) + psql "$DATABASE_URL" # 본인 전용 스키마로 바로 접속됨 + ``` +- **참고**: DB(`$DATABASE_URL`)·S3 접속정보는 `.project-env` 에 이미 들어 있고, 프로젝트 폴더에 들어가면 자동 적용됩니다. 코드에서는 그냥 `process.env.DATABASE_URL` 등으로 쓰면 됩니다. + +### 8-1. AI 도구를 잘 쓰는 법 (팁) + +- **항상 프로젝트 폴더 안에서 실행**하세요. AI 는 "지금 폴더"의 파일을 읽어 맥락을 잡습니다. +- **`CLAUDE.md` 를 활용**하세요. sample 에는 이미 `CLAUDE.md`(이 프로젝트 규칙: podman 개발, Coolify 배포, 비밀값 금지 등)가 들어 있고, Claude 가 자동으로 읽습니다. 프로젝트 규칙·주의사항을 여기에 적어두면 AI 가 그대로 따릅니다. +- **구체적으로 시키세요**. "로그인 API 만들어줘" 보다 "`src/` 에 POST /login 엔드포인트 추가, 입력 검증하고 실패 시 401 반환" 처럼. +- **확인은 직접**: AI 가 만든 코드도 9번(로컬 실행)·10번(컨테이너)으로 **반드시 본인이 동작 확인** 후 커밋하세요. +- 키가 안 먹으면(401) → 5번 `update-litellm-key`. 모델 오류(400)면 → 5번 표의 모델명. + +### 8-2. bkit — AI 개발 보조 플러그인 (선택, 권장) + +**bkit**(Vibecoding Kit, https://www.bkit.ai/ )은 Claude Code 에 **체계적 개발 절차(PDCA: 계획→설계→구현→검증)** 와 다수의 전문 스킬을 더해주는 플러그인입니다. "무엇을 만들지"만 설명하면 bkit 이 계획부터 구현·검증까지 단계적으로 진행해 줍니다. + +**설치 (최초 1회, Claude Code 안에서 입력)** +``` +/plugin marketplace add popup-studio-ai/bkit-claude-code +/plugin install bkit +``` + +**자주 쓰는 명령** (Claude Code 프롬프트에 입력) +- `/pdca pm <기능이름>` — 기능 하나를 계획→구현→검증까지 한 번에. **처음엔 이거 하나면 충분**합니다. +- `/sprint` — 여러 기능을 묶은 릴리스 단위 작업. +- `/control` — AI 가 얼마나 자동으로 진행할지(자율도) 조절. + +> bkit 의 세부 절차를 몰라도 됩니다 — **원하는 걸 자연어로 말하면** bkit 이 알맞은 흐름을 골라 줍니다. +> 더 알아보려면 공식 사이트(bkit.ai) / GitHub(`popup-studio-ai/bkit-claude-code`) 참고. + +--- + +## 9. 로컬에서 실행하고 브라우저로 확인하기 + +코드를 바로 실행해 동작을 확인하는 단계입니다(가장 빠름). + +> **빌드·테스트는 4단계로 점점 "실제 배포에 가깝게" 검증합니다.** +> | 단계 | 무엇으로 | 무엇을 확인 | 어디서 | +> |---|---|---|---| +> | **9. 소스 실행** | `npm run dev` | 코드 로직 (가장 빠름) | 워크스페이스 | +> | **10. 컨테이너 빌드** | `podman build`+`run` | Dockerfile 이 제대로 빌드/기동되는지 | 워크스페이스 | +> | **11. Git push** | `git push` | 배포에 쓸 코드를 저장소에 올림 | Gitea | +> | **12. 배포 빌드** | Coolify | 실제 서비스로 빌드/기동 | Coolify | +> 앞 단계가 통과해야 뒤 단계가 거의 그대로 됩니다(같은 코드·같은 Dockerfile). 막히면 **앞 단계로 돌아가** 고치세요. + +**1) 실행** +```bash +cd ~/projects/<본인 프로젝트> +npm run dev # 코드를 고치면 자동으로 다시 시작됨(핫리로드) +``` +실행되면 터미널에 `sample listening on :3000` 같은 줄이 뜹니다. **에러 없이** 이 줄이 보이면 기동 성공입니다. +(빨간 에러가 뜨면 거의 ① `npm install` 안 함 ② 코드 문법 오류 ③ `.project-env` 미로딩(=폴더 밖에서 실행) 셋 중 하나입니다.) + +**2) 연결만 빠르게 점검**(앱 안 띄우고 DB/파일저장소 자격증명·연결 확인): +```bash +npm run db:check # → "DB OK: { now: 2026-... }" 이면 DB 연결 정상 +npm run minio:check # → "S3 OK: { bucket: 'coolify-user-data', sampleKeys: [...] }" 이면 정상 +``` +> `DB FAIL` / `S3 FAIL` 이 나오면 `.project-env` 값(host·키)을 확인하세요. **여기서 통과하면 배포 환경에서도 거의 됩니다.** + +**3) 브라우저로 미리보기** — 워크스페이스는 사내 클러스터 안이라 `localhost:3000` 이 PC 브라우저엔 바로 안 열립니다. VS Code 의 포트 기능을 씁니다: +1. VS Code 하단 **`PORTS`** 탭 클릭 → **`Forward a Port`** → 포트 번호(`3000`) 입력 + (앱이 뜨면 자동 감지해 알림이 뜨기도 합니다) +2. 포워딩된 포트 옆 **🌐 (Open in Browser)** 클릭 → 새 탭에 앱이 열립니다. + - 열리는 주소: `https://<자동생성>.coder.bokdev.in` (본인 전용 임시 URL, 사내 정식 TLS) + +**4) 엔드포인트로 동작 확인** — 브라우저 주소 뒤에 경로를 붙이거나, 터미널에서 `curl` 로 확인합니다. **각 응답의 `"ok": true` 와 아래 기대값을 확인**하세요: + +| 경로 | 의미 | 정상 응답(예) | +|---|---|---| +| `/healthz` | 앱이 살아있나 | `{"ok":true}` | +| `/db` | 내 Postgres 연결 | `{"ok":true,"now":"2026-..."}` | +| `/s3` | 파일저장소(MinIO) 연결 | `{"ok":true,"bucket":"coolify-user-data","sampleKeys":[...]}` | + +```bash +curl 127.0.0.1:3000/healthz # {"ok":true} +curl 127.0.0.1:3000/db # DB 연결 + 현재시각 +curl 127.0.0.1:3000/s3 # 버킷명 + 파일목록 일부 +``` +> `"ok": false` + `error` 가 보이면 그 메시지가 원인입니다(예: DB 비번 틀림, 버킷 없음). `/db 500` 은 보통 `.project-env` 의 `DATABASE_URL` 문제입니다. +> 이 임시 URL 은 본인만 접근 가능하고 워크스페이스를 끄면 사라집니다. **미리보기용**이며, 정식 배포는 12번입니다. + +--- + +## 10. 컨테이너로 빌드해서 확인하기 (podman) — 배포 전 점검 + +9번은 코드를 그냥 실행한 것이고, 실제 배포(Coolify)는 **Dockerfile 로 컨테이너를 빌드**해서 띄웁니다. +배포 전에 **같은 방식(컨테이너)으로 한 번 돌려보면** 배포 후 문제를 미리 잡을 수 있습니다. +(Coolify 도 똑같은 `Dockerfile` 을 쓰므로, **여기서 빌드가 되면 12번 배포 빌드도 거의 됩니다.**) + +**1) 빌드** +```bash +cd ~/projects/<본인 프로젝트> +podman build -t myapp . # 현재 폴더의 Dockerfile 로 이미지 빌드 +``` +(`docker build ...` 도 동일하게 동작합니다 — 실제 런타임이 podman 일 뿐입니다.) + +**빌드 로그 읽는 법** — 한 줄씩 `STEP 1/9`, `STEP 2/9` … 식으로 진행됩니다(이게 Dockerfile 의 각 명령). 마지막에 +``` +COMMIT myapp +Successfully tagged localhost/myapp:latest +<이미지ID> +``` +가 보이면 **빌드 성공**입니다. 빌드된 이미지는 `podman images` 로 확인할 수 있습니다. +- **실패하면** 빨간 `Error:` 줄과 **몇 번째 STEP 에서 멈췄는지**를 보세요. 가장 흔한 건 `npm ci`/`npm install` 단계 실패(=의존성 문제, `package.json` 확인)입니다. + +**2) 실행** +```bash +podman run --rm -p 3000:3000 --env-file .project-env myapp # 빌드한 이미지를 컨테이너로 실행 +# 또는 (빌드+실행 한 번에): podman compose up --build +``` +- `--env-file .project-env` 로 DB/S3 값을 컨테이너에 넣어줍니다(이게 없으면 컨테이너 안에서 `/db`·`/s3` 가 실패합니다). +- 기동되면 9번과 똑같이 `sample listening on :3000` 이 보입니다. + +**3) 동작 확인** — 컨테이너 접속 확인은 **`127.0.0.1`** 로 하세요(`localhost` 가 간혹 안 잡힙니다). 기대 응답은 9번 표와 동일합니다: +```bash +curl 127.0.0.1:3000/healthz # {"ok":true} +curl 127.0.0.1:3000/db # {"ok":true,"now":"2026-..."} +curl 127.0.0.1:3000/s3 # {"ok":true,"bucket":"coolify-user-data",...} +``` +- 브라우저로 보려면 9번과 동일하게 **PORTS 패널 → Forward Port 3000 → Open in Browser**(`https://<자동생성>.coder.bokdev.in`). +- 확인이 끝나면 터미널에서 **Ctrl+C** 로 컨테이너를 멈춥니다(`--rm` 이라 자동 삭제됨). +- 빌드/실행 권한은 워크스페이스에 이미 설정돼 있습니다(별도 설정 불필요). + +> 여기서 **빌드 성공 + 3개 엔드포인트 모두 `ok:true`** 면 Coolify 배포도 거의 그대로 됩니다. 안 되면 코드/Dockerfile 을 먼저 고치세요. + +--- + +## 11. Gitea(git)에 올리기 + +`new-project` 로 만든 폴더는 아직 git 저장소가 아닙니다(`.git` 없음). 아래처럼 올립니다. +(배포(12번)는 Gitea repo 를 받아서 빌드하므로, 배포 전에 반드시 올려야 합니다.) + +> 아래 명령의 `myapp` 은 **본인이 `new-project` 로 만든 프로젝트 이름**으로 바꿔 쓰세요. + +**1) Gitea에 빈 저장소 만들기** +- **https://gitea.bokdev.in** → 우측 상단 **`+` → New Repository** +- Repository Name 입력(예: `myapp`) +- **README/.gitignore/License 는 체크하지 마세요**(빈 저장소여야 충돌이 없습니다) → Create +- 생성되면 주소가 나옵니다: `https://gitea.bokdev.in/<본인사번>/myapp.git` + +**2) 워크스페이스 터미널에서 올리기** +```bash +cd ~/projects/myapp +git init +git add . +git commit -m "first commit" +git branch -M main +git remote add origin https://gitea.bokdev.in/<본인사번>/myapp.git +git push -u origin main +``` +- 인증은 자동입니다(6번 Gitea 승인을 한 번 했다면). 처음이면 승인 화면이 한 번 뜹니다. +- 이후 수정한 뒤에는 `git add . && git commit -m "..." && git push` 만 반복하면 됩니다. + +> **순서는 상관없습니다.** 코드를 먼저 만들고(권장) 나중에 저장소를 만들어도 됩니다. `git push` 시점에 Gitea 저장소만 있으면 됩니다. +> **`.project-env` 는 git에 올라가지 않습니다**(DB·S3 자격증명 보호 — 정상). `git status` 에 안 보여도 맞습니다. 배포 환경값은 Coolify에 따로 넣습니다(12번). + +--- + +## 12. Coolify로 배포하고 확인하기 + +운영(실제 서비스) 배포는 **Coolify**가 담당합니다. 흐름은 "**Gitea에 push → Coolify가 Dockerfile로 자동 빌드·배포**" 입니다. (먼저 11번으로 Gitea에 올려두세요.) + +**1) 앱 만들기** +1. **Coolify**(https://coolify.bokdev.in) 로그인. +2. 새 리소스 생성 → **Git 기반(Public)** → 빌드 방식 **Dockerfile**. + - **Repository URL 은 반드시 전체 주소**로 입력: `https://gitea.bokdev.in/<본인사번>/myapp.git` + (`<본인사번>/myapp` 처럼 줄여 쓰면 clone 이 실패합니다.) + - Branch: `main`, Port: `3000` +3. **도메인(공개 주소) 지정** — 앱의 **Configuration → Domains**: + - **`Generate Domain`(자동 생성) 버튼을 누르면** `https://<랜덤>.apps.bokdev.in` 형태로 **자동 입력**됩니다(이 서버 기본 도메인이 `apps.bokdev.in` 으로 설정돼 있음). 그대로 써도 됩니다. + - 원하는 이름으로 바꾸려면 **`https://<원하는이름>.apps.bokdev.in`** 으로 직접 입력하세요(예: `https://myapp.apps.bokdev.in`). + - 반드시 **`.apps.bokdev.in` 로 끝나는** 주소여야 외부에서 https 로 열립니다(와일드카드 TLS·라우팅이 이 대역에만 준비돼 있음). `<이름>` 은 **남과 겹치지 않게**(예: `사번-앱이름`). 미리보기(9번)의 `*.coder.bokdev.in` 과는 별개 대역입니다. + +**2) 환경변수 입력** (앱마다 **최초 1회만**, 이후 배포엔 유지됨) + - Coolify의 **Environment Variables** 에 입력: `DATABASE_URL`, `S3_ENDPOINT`, `S3_REGION`, `S3_BUCKET`, `S3_ACCESS_KEY_ID`, `S3_SECRET_ACCESS_KEY`, `PORT` + - **가장 쉬운 방법: 워크스페이스 `.project-env` 파일을 열어 그 안의 값을 한 줄씩 그대로 복사**해 넣으세요(DB host 포함 동일, 변형 금지). + - 파일 보기: 터미널에서 `cat ~/projects/myapp/.project-env` (또는 VS Code 에서 그 파일 열기) + - `DATABASE_URL` 은 아래처럼 생겼습니다. **`%20`·`%3D` 까지 그대로** 넣으세요(libpq 인코딩이라 빼면 DB 연결이 깨집니다): + ``` + DATABASE_URL=postgresql://emp_<사번>:@10.200.0.152:5432/appdb?options=-c%20search_path%3Demp_<사번> + ``` + +**3) 배포(빌드) 실행 + 로그 보기** +1. 앱 화면에서 **Deploy** 클릭. +2. **Deployments** 탭(또는 우측 알림)에서 방금 빌드를 누르면 **빌드 로그가 실시간으로** 흐릅니다. 단계는 대략: + - `Cloning ...` (Gitea 에서 코드 받기) → `Building image ...`(여러분의 `Dockerfile` 로 빌드, 10번 podman build 와 같은 STEP 들) → `Starting container ...` → **`New container started`** / `Deployment finished` 가 보이면 **배포 성공**. +3. **실패하면 로그의 빨간 줄**을 보세요. 위치로 원인이 갈립니다: + - `Cloning` 에서 실패 → Repository URL(전체 주소) 문제 (FAQ `does not appear to be a git repository`). + - `Building` 에서 실패 → Dockerfile/의존성 문제. **10번에서 `podman build` 가 됐다면 여기서도 거의 됩니다** → 안 되면 push 한 코드가 최신인지(11번) 확인. + - 빌드는 됐는데 컨테이너가 `Unhealthy`/재시작 반복 → 보통 Env 누락(아래 4번 확인). + +**4) 배포된 앱 주소 확인 + 테스트** +1. 앱 주소는 **1)-3에서 지정한 `https://<원하는이름>.apps.bokdev.in`** 입니다(배포가 끝나면 그 주소로 외부에서 열립니다). +2. 그 주소로 **9·10번과 똑같이** 엔드포인트를 확인합니다(브라우저 또는 PC 터미널 `curl`): + ```bash + curl https://<원하는이름>.apps.bokdev.in/healthz # {"ok":true} ← 앱 기동 OK + curl https://<원하는이름>.apps.bokdev.in/db # {"ok":true,"now":...} ← DB Env OK + curl https://<원하는이름>.apps.bokdev.in/s3 # {"ok":true,"bucket":...} ← S3 Env OK + ``` + - `/healthz` 만 되고 `/db`·`/s3` 가 500 이면 → **Coolify Env 문제**(2번). `.project-env` 값과 정확히 같은지(특히 `DATABASE_URL` 의 `%20`/`%3D`) 다시 확인하고 재배포. +3. 이후 코드를 고쳐 **`git push` 하면 자동으로 다시 빌드·배포**됩니다(Deployments 에서 새 빌드 로그 확인 → 같은 주소로 재확인). + +> 자주 막히는 것: ① Repository URL 전체주소 ② `DATABASE_URL` host 가 `10.200.0.152` + 인코딩(`%20`/`%3D`) 그대로인지 ③ Env 를 앱에 저장했는지 ④ 도메인을 `.apps.bokdev.in` 으로 지정했는지. (FAQ 참고) + +--- + +## 13. 워크스페이스 켜고 끄기 + +- **그만 쓸 때**: Coder 워크스페이스 화면 → **Stop** (자원 절약. 파일은 보존됩니다.) +- **다시 쓸 때**: **Start** (수십 초 내 기동) +- **업데이트 안내가 뜨면**(`Update` 버튼): 눌러서 최신 환경으로 갱신하세요. `~/projects` 파일은 유지됩니다. + +--- + +## 자주 묻는 것 +- **Q. 파일이 사라졌어요** → `~/projects` 밖에 저장했을 가능성. 작업물은 항상 `~/projects` 아래에. +- **Q. AI 도구가 인증 오류(401)** → `update-litellm-key` 를 다시 실행해 본인 키를 입력하세요 (5번). 키가 `sk-` 로 시작하는지 확인. +- **Q. AI 도구가 `Invalid model` 오류(400)** → 인증은 됐지만 모델명이 게이트웨이에 없는 경우. 5번 표의 기본 모델명을 쓰세요. +- **Q. `$DATABASE_URL` 이 비어있어요** → 프로젝트 폴더 안에서 실행했는지 확인하세요. `.project-env` 는 그 폴더에 `cd` 해야 적용됩니다(7번). +- **Q. sample 을 고쳤는데 git pull 이 안 돼요** → sample 은 참조용(읽기 전용)입니다. `new-project <이름>` 으로 복사한 폴더에서 작업하세요(7번). +- **Q. git push가 인증을 물어봐요** → Gitea Authorize를 한 번도 안 했을 때. 6번 참고. +- **Q. Coolify 배포가 `does not appear to be a git repository` 로 실패해요** → Repository URL 을 전체 주소(`https://gitea.bokdev.in/...git`)로 넣었는지 확인하세요(12번). +- **Q. 배포한 앱에서 DB 연결이 안 돼요(`/db` 500)** → ① Coolify Env 의 `DATABASE_URL` host 가 `10.200.0.152` 인지, ② 끝의 `?options=-c%20search_path%3D...` 가 `%20`/`%3D` 까지 그대로인지 확인하세요. 직접 타이핑하다 인코딩을 빼면 깨집니다 — `.project-env` 값을 그대로 복사하는 게 가장 안전합니다(12번). 컨테이너명/내부 DNS 는 배포 환경에선 안 됩니다. +- **Q. `npm run dev` 가 `Cannot find package 'express'` 로 실패해요** → 처음 1회 `npm install` 을 안 한 경우입니다. 프로젝트 폴더에서 `npm install` 후 다시 실행하세요(7번). +- **Q. `podman build` 가 `npm ci`/`npm install` 단계에서 실패해요** → 의존성 문제입니다. 먼저 워크스페이스에서 `npm install` 이 되는지 확인하고, `package.json` 에 빠진 패키지가 없는지 보세요(10번). +- **Q. Coolify 빌드는 성공했는데 앱이 안 떠요(Unhealthy/재시작 반복)** → 보통 Env 누락입니다. `/healthz` 만 확인해 보고, DB/S3 Env(2번)를 `.project-env` 그대로 넣었는지 확인 후 재배포하세요(12번). +- **Q. Coolify 가 옛날 코드로 빌드돼요** → `git push` 가 됐는지(11번), Coolify 의 Branch 가 `main` 인지 확인하세요. push 후 Deploy(또는 자동배포)를 다시 도세요. +- **Q. 배포된 앱 주소가 안 열려요(접속 안 됨 / 인증서 경고)** → 도메인을 **`.apps.bokdev.in` 으로 끝나게** 지정했는지 확인하세요(12번 1)-3). 그 대역만 외부 https 가 준비돼 있습니다. 다른 대역(예: `*.bokdev.in` 루트, 임의 도메인)은 안 열리거나 인증서 경고가 납니다. 이름이 남과 겹쳐도 충돌하니 `사번-앱이름` 처럼 고유하게 정하세요. +- **Q. 빌드/실행은 됐는데 브라우저로 안 열려요** → 워크스페이스 앱은 `localhost` 가 PC 에서 안 열립니다. VS Code **PORTS → Forward 3000 → Open in Browser**(`https://<자동생성>.coder.bokdev.in`)로 여세요(9번). 배포된 앱은 Coolify 가 준 공개 주소로 엽니다(12번). +- **Q. DB가 비어있어요** → 정상입니다. 본인 전용 빈 스키마가 제공됩니다. 테이블은 직접 만들면 됩니다. +- **Q. K8s(쿠버네티스)는 어떻게 봐요?** → 직원은 K8s에 직접 접근하지 않습니다. DB/스토리지/배포는 위 도구들로 충분합니다. + +--- + +문의: 인프라 담당자 diff --git a/README.md b/README.md new file mode 100644 index 0000000..3b72fcf --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# sample — Node 앱 샘플 (podman 개발 + Coolify 배포) + +사내 워크스페이스용 Node(ESM) 샘플. DB·MinIO 연결, podman 로컬개발, Coolify 자동배포가 미리 잡혀 있다. +이 폴더(`~/projects/sample`)는 **읽기 전용 참조**다. 직접 고치지 말고, 새 작업은 아래처럼 복사해서 시작한다. + +## 새 프로젝트 시작 +```bash +new-project myapp # sample 을 ~/projects/myapp 으로 복사 + .project-env(DB/S3) 자동생성 +cd ~/projects/myapp # 들어오면 셸이 .project-env 를 자동 export ($DATABASE_URL 등 사용 가능) +npm install +npm run dev # http://localhost:3000 +``` +- `GET /healthz` — 헬스체크 +- `GET /db` — Postgres 연결 확인 (직원 전용 schema) +- `GET /s3` — MinIO 버킷 연결 확인 + +## 컨테이너로 실행 (podman) +```bash +podman build -t sample . +podman run --rm -p 3000:3000 --env-file .project-env sample +# 또는: podman compose up --build +``` + +## 배포 (Coolify) +Gitea에 push → Coolify가 `Dockerfile`로 자동 빌드/배포. Coolify Env에 `DATABASE_URL`, `S3_*` 입력. + +## 연결정보 +- **DB**: 프로젝트 `.project-env` 의 `DATABASE_URL` (본인 schema 격리, 폴더 cd 시 자동 export) +- **MinIO(S3)**: endpoint `https://minio.bokdev.in`, bucket `coolify-user-data`, 키는 `.project-env`에 있음 +- **AI**: `~/.env` 의 `LITELLM_KEY`(`update-litellm-key` 명령으로 입력) → Claude Code/Codex/Gemini 공용 + +자세한 작업 규약은 `CLAUDE.md` 참고. diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..82b4117 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,17 @@ +# 로컬 개발용 (podman compose 또는 podman play). 운영 배포는 Coolify가 담당. +# podman compose up --build +# DB/MinIO 접속정보는 프로젝트 .project-env 에서 주입됨(프로젝트 폴더 cd 시 셸이 자동 export). +services: + app: + build: . + ports: + - "3000:3000" + env_file: + - .project-env + environment: + # 워크스페이스에 자동 주입된 값을 컨테이너로 전달 + DATABASE_URL: ${DATABASE_URL} + S3_ENDPOINT: ${S3_ENDPOINT:-https://minio.bokdev.in} + S3_BUCKET: ${S3_BUCKET:-coolify-user-data} + S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID} + S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY} diff --git a/docs/01-plan/features/todo-app.plan.md b/docs/01-plan/features/todo-app.plan.md new file mode 100644 index 0000000..8956332 --- /dev/null +++ b/docs/01-plan/features/todo-app.plan.md @@ -0,0 +1,131 @@ +# Plan — todo-app (간단한 To-Do 앱) + +> PDCA Phase: **Plan** · Feature: `todo-app` · 작성일: 2026-06-15 +> 스택: Node 22 (ESM) + Express · Postgres(`src/db.js`) · MinIO/S3(`src/s3.js`) · podman 로컬 / Coolify 배포 + +## Executive Summary + +| 관점 | 요약 | +|------|------| +| **Problem** | 할 일을 기록·관리할 화면과 저장소가 없다. 현재 앱은 연결 확인용 엔드포인트(`/db`, `/s3`)만 존재한다. | +| **Solution** | Postgres에 할 일(todo)을 CRUD로 저장하고, 할 일별 첨부파일은 MinIO/S3에 저장하는 단일 공용 To-Do 웹 앱. 기존 `db.js`/`s3.js`/`config.js`를 그대로 확장한다. | +| **Function UX Effect** | 브라우저에서 할 일 추가/완료/삭제, 파일 첨부·다운로드가 즉시 동작. REST API도 함께 제공해 curl/외부 연동 가능. | +| **Core Value** | DB(구조화 데이터) + S3(바이너리 파일)를 함께 쓰는 가장 단순하고 실용적인 표준 패턴을 적은 코드로 제공. | + +## Context Anchor + +| 항목 | 내용 | +|------|------| +| **WHY** | 사내 워크스페이스 샘플을 실제로 쓸 수 있는 최소 To-Do 앱으로 만들어 DB+S3 활용 패턴을 보여준다. | +| **WHO** | 로그인 없이 접근하는 단일 사용자/소규모 팀(단일 공용 목록). | +| **RISK** | 첨부파일 처리(업로드 크기/타입, presigned URL), schema 마이그레이션 누락, S3 키-DB 레코드 정합성. | +| **SUCCESS** | 브라우저에서 할 일 CRUD + 첨부 업로드/다운로드가 동작하고 `npm run db:check`/`minio:check`가 통과. | +| **SCOPE** | IN: todo CRUD, 첨부 1개/항목, 웹 UI, REST API. OUT: 인증/멀티유저, 마감일/태그/검색, 실시간 동기화. | + +--- + +## 1. 배경 및 목표 + +기존 `src/server.js`는 `/healthz`, `/db`, `/s3`, `/` 만 제공한다. 여기에 To-Do 도메인을 얹는다. + +- **목표**: "DB와 S3를 이용한 간단한 To-Do 앱"을 최소 변경으로 구현. +- **설계 원칙**: 외부 연결은 반드시 `src/config.js`/`src/db.js`/`src/s3.js`를 경유(CLAUDE.md 규약). 비밀값은 env에서만. +- **데이터 분담**: 구조화 데이터(todo 본문/상태) → Postgres, 바이너리(첨부파일) → MinIO/S3, 둘을 키로 연결. + +## 2. 요구사항 + +### 2.1 기능 요구사항 (FR) + +| ID | 요구사항 | 우선순위 | +|----|----------|:--------:| +| FR-01 | 할 일 목록 조회 (최신순) | Must | +| FR-02 | 할 일 추가 (제목, 선택적 첨부파일 1개) | Must | +| FR-03 | 할 일 완료/미완료 토글 | Must | +| FR-04 | 할 일 삭제 (연결된 S3 첨부도 함께 삭제) | Must | +| FR-05 | 첨부파일 업로드 → MinIO/S3 저장, DB에 키 기록 | Must | +| FR-06 | 첨부파일 다운로드 (presigned URL 또는 프록시 스트리밍) | Must | +| FR-07 | 간단한 웹 UI(HTML) — 목록/추가/완료/삭제/첨부 | Must | +| FR-08 | REST API(JSON) — 위 동작을 curl로도 수행 가능 | Should | + +### 2.2 비기능 요구사항 (NFR) + +| ID | 요구사항 | +|----|----------| +| NFR-01 | ESM, Node 22+, 외부 연결은 config/db/s3 경유 (CLAUDE.md 규약 준수) | +| NFR-02 | 비밀값 코드/커밋 금지 — 모두 `.project-env` env에서 주입 | +| NFR-03 | 업로드 파일 크기 제한(예: 10MB) 및 기본 검증 | +| NFR-04 | podman 빌드/실행 및 Coolify 동일 Dockerfile에서 동작 | +| NFR-05 | DB schema는 앱 시작 시 멱등(idempotent) 생성 또는 마이그레이션 스크립트 제공 | + +## 3. 범위 (Scope) + +- **In scope**: todo CRUD, 항목당 첨부파일 1개(S3), 서버 렌더 웹 UI, JSON REST API, schema 부트스트랩. +- **Out of scope**: 인증/멀티유저, 마감일·태그·우선순위·검색, 다중 첨부, 실시간(WebSocket), 페이지네이션. + +## 4. 데이터 모델 (초안) + +``` +table todo ( + id uuid pk default gen_random_uuid(), + title text not null, + done boolean not null default false, + attachment_key text null, -- S3 object key (없으면 null) + attachment_name text null, -- 원본 파일명 + attachment_type text null, -- content-type + created_at timestamptz not null default now() +) +``` +- 첨부 S3 키 규칙(초안): `todos/{todo_id}/{원본파일명}` (버킷 `coolify-user-data`). +- 삭제 시 DB row 삭제 + S3 object 삭제(정합성). + +## 5. API 설계 (초안) + +| Method | Path | 설명 | +|--------|------|------| +| GET | `/` | 웹 UI (할 일 목록 페이지) | +| GET | `/api/todos` | 목록(JSON) | +| POST | `/api/todos` | 추가 (multipart: title + 선택 file) | +| PATCH | `/api/todos/:id` | done 토글 | +| DELETE | `/api/todos/:id` | 삭제(+ S3 첨부 삭제) | +| GET | `/api/todos/:id/attachment` | 첨부 다운로드(presigned redirect 또는 스트리밍) | + +## 6. 구현 항목 (모듈 단위) + +| 모듈 | 작업 | 신규/수정 | +|------|------|:---------:| +| `src/schema.js` (또는 `scripts/migrate.js`) | todo 테이블 멱등 생성 | 신규 | +| `src/todos.repo.js` | DB CRUD 쿼리 (pool 사용) | 신규 | +| `src/attachments.js` | S3 업로드/삭제/presigned URL (`s3.js` 확장) | 신규 | +| `src/routes.todos.js` | REST 라우트 핸들러 | 신규 | +| `src/server.js` | 라우트/정적·multipart 미들웨어 연결 | 수정 | +| `public/index.html` (또는 인라인 템플릿) | 간단한 웹 UI | 신규 | +| `package.json` | `multer`(업로드), `@aws-sdk/s3-request-presigner` 의존성 추가 | 수정 | + +## 7. 의존성 + +- 추가 예정: `multer`(multipart 업로드 파싱), `@aws-sdk/s3-request-presigner`(다운로드 URL). 추가 후 `npm install`로 lock 갱신. +- 기존: `express`, `pg`, `@aws-sdk/client-s3` 재사용. + +## 8. Success Criteria + +| SC | 기준 | 검증 방법 | +|----|------|-----------| +| SC-01 | 브라우저에서 할 일 추가/완료/삭제가 동작 | 수동 + L2 UI 테스트 | +| SC-02 | 첨부파일 업로드 시 S3에 객체 생성, DB에 key 기록 | `/s3` 또는 MinIO 확인 + DB row | +| SC-03 | 첨부 다운로드가 정상 동작 | 다운로드 후 파일 일치 확인 | +| SC-04 | 할 일 삭제 시 S3 객체도 삭제 | 삭제 후 S3 키 부재 확인 | +| SC-05 | `npm run db:check` / `npm run minio:check` 통과 | CLI 실행 | +| SC-06 | `podman build` + `podman run --env-file .project-env`로 동일 동작 | 컨테이너 기동 확인 | + +## 9. 리스크 및 대응 + +| 리스크 | 대응 | +|--------|------| +| `gen_random_uuid()` 미존재(pgcrypto) | `pgcrypto` 확장 활성 확인 또는 앱에서 uuid 생성 | +| 업로드 대용량/악성 파일 | 크기 제한(10MB), content-type 화이트리스트 검토 | +| DB-S3 정합성(삭제 누락) | 삭제는 S3 먼저 시도 후 DB 삭제, 실패 시 로깅 | +| presigned URL 만료/내부 호스트 노출 | 짧은 만료 또는 서버 프록시 스트리밍 중 Design에서 택1 | + +## 10. 다음 단계 + +`/pdca design todo-app` — 3가지 아키텍처 옵션(최소변경 / 클린 / 실용균형)을 비교하고 데이터 모델·API·다운로드 방식을 확정. diff --git a/docs/02-design/features/todo-app.design.md b/docs/02-design/features/todo-app.design.md new file mode 100644 index 0000000..6ad4872 --- /dev/null +++ b/docs/02-design/features/todo-app.design.md @@ -0,0 +1,203 @@ +# Design — todo-app (간단한 To-Do 앱) + +> PDCA Phase: **Design** · Feature: `todo-app` · 작성일: 2026-06-15 +> 선택 아키텍처: **Option C — 실용 균형** · 첨부 다운로드: **서버 프록시 스트리밍** +> 상위 문서: [Plan](../../01-plan/features/todo-app.plan.md) + +## Context Anchor + +| 항목 | 내용 | +|------|------| +| **WHY** | 사내 워크스페이스 샘플을 실제로 쓸 수 있는 최소 To-Do 앱으로 만들어 DB+S3 활용 패턴을 보여준다. | +| **WHO** | 로그인 없이 접근하는 단일 사용자/소규모 팀(단일 공용 목록). | +| **RISK** | 첨부파일 처리(크기/타입), schema 마이그레이션 누락, S3 키-DB 레코드 정합성. | +| **SUCCESS** | 브라우저에서 할 일 CRUD + 첨부 업로드/다운로드가 동작하고 `npm run db:check`/`minio:check` 통과. | +| **SCOPE** | IN: todo CRUD, 첨부 1개/항목, 웹 UI, REST API. OUT: 인증/멀티유저, 마감일/태그/검색, 실시간. | + +--- + +## 1. Overview + +기존 Express 앱(`src/server.js`)에 To-Do 도메인을 추가한다. 구조화 데이터는 Postgres(`src/db.js` pool), +첨부파일 바이너리는 MinIO/S3(`src/s3.js` client)에 저장하고 두 저장소를 S3 object key로 연결한다. + +**아키텍처 결정 (Option C — 실용 균형)**: route / repo / attachments 3개 모듈로 관심사를 분리하되, +별도 service 계층은 두지 않는다(라우트 핸들러가 repo+attachments를 조합). "간단한 앱" 목표에 맞는 최소한의 경계. + +**다운로드 결정 (서버 프록시 스트리밍)**: 브라우저는 앱 엔드포인트에만 요청하고, 서버가 `GetObjectCommand`로 +S3 객체를 받아 응답 스트림으로 전달. 내부 MinIO 호스트(`minio.bokdev.in`) 노출 없이 환경 무관하게 동작. +→ presigned URL 불필요, `@aws-sdk/s3-request-presigner` 의존성 추가하지 않음. + +## 2. 모듈 구조 + +``` +src/ + config.js (기존) 변경 없음 + db.js (기존) pool, pingDb — 재사용 + s3.js (기존) s3 client, pingS3 — 재사용. GetObject/PutObject/DeleteObject 추가 export + schema.js (신규) todo 테이블 멱등 생성 (앱 시작 시 ensureSchema 호출) + todos.repo.js (신규) DB CRUD 쿼리 (pool 사용) + attachments.js (신규) S3 put / get(stream) / delete 래퍼 + routes.todos.js (신규) Express Router — REST 핸들러 + server.js (수정) express.json/static 미들웨어, schema 부트스트랩, 라우터 마운트 +public/ + index.html (신규) 단일 페이지 웹 UI (vanilla JS fetch) +``` + +## 3. 데이터 모델 + +```sql +-- schema.js 에서 CREATE TABLE IF NOT EXISTS 로 멱등 실행 +CREATE TABLE IF NOT EXISTS todo ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + title text NOT NULL, + done boolean NOT NULL DEFAULT false, + attachment_key text, -- S3 object key (없으면 NULL) + attachment_name text, -- 원본 파일명 + attachment_type text, -- content-type + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_todo_created_at ON todo (created_at DESC); +``` + +- `gen_random_uuid()`는 pgcrypto 확장이 필요. 미존재 환경 대비: schema.js에서 `CREATE EXTENSION IF NOT EXISTS pgcrypto`를 + 먼저 시도하고, 권한 부족으로 실패하면 앱에서 `crypto.randomUUID()`로 id를 생성해 INSERT (fallback). +- **S3 키 규칙**: `todos/{todo_id}/{원본파일명}` — 버킷은 `config.s3.bucket`(`coolify-user-data`). + +## 4. API 계약 + +| Method | Path | Request | Response (성공) | 비고 | +|--------|------|---------|------|------| +| GET | `/` | — | `text/html` (index.html) | 정적 제공 | +| GET | `/api/todos` | — | `200 { data: Todo[] }` | created_at DESC | +| POST | `/api/todos` | `multipart/form-data`: `title`(필수), `file`(선택) | `201 { data: Todo }` | file 있으면 S3 업로드 후 key 기록 | +| PATCH | `/api/todos/:id` | `application/json`: `{ done: boolean }` | `200 { data: Todo }` | 토글 | +| DELETE | `/api/todos/:id` | — | `204` | S3 첨부 먼저 삭제 후 DB row 삭제 | +| GET | `/api/todos/:id/attachment` | — | `200` 스트림(`Content-Type`, `Content-Disposition`) | 프록시 스트리밍, 첨부 없으면 404 | + +**공통 규약** +- 성공 바디: `{ data: ... }`, 에러 바디: `{ error: string }`. +- 입력 검증: `title` 비어있으면 `400 { error }`. 잘못된 `:id`(uuid 형식 아님) → `400`. 없는 todo → `404`. +- 업로드 제한: multer `limits.fileSize = 10MB`. 초과 시 `413 { error }`. + +### Todo 객체 형태 +```json +{ + "id": "uuid", + "title": "string", + "done": false, + "attachment_name": "report.pdf | null", + "attachment_type": "application/pdf | null", + "hasAttachment": true, + "created_at": "ISO8601" +} +``` +> `attachment_key`는 응답에서 제외(내부 키 비노출), 대신 `hasAttachment` boolean 노출. + +## 5. 처리 흐름 (핵심 시퀀스) + +**POST /api/todos (첨부 포함)** +1. multer가 `title` + `file`(메모리 버퍼) 파싱 → title 검증 +2. id 생성(또는 INSERT 후 반환된 id) → S3 key `todos/{id}/{filename}` 결정 +3. `attachments.put(key, buffer, contentType)` → S3 업로드 +4. repo: INSERT (attachment_key/name/type 포함) → 201 반환 +5. 3단계 실패 시 DB INSERT 하지 않음(또는 보상 삭제) → 500 + +**DELETE /api/todos/:id** +1. repo: SELECT attachment_key +2. key 있으면 `attachments.del(key)` 먼저 시도(실패는 로깅하되 계속) +3. repo: DELETE row → 204 + +**GET /api/todos/:id/attachment** +1. repo: SELECT attachment_key/name/type → 없으면 404 +2. `attachments.getStream(key)` → `Content-Type`, `Content-Disposition: attachment; filename=...` 세팅 +3. S3 Body 스트림을 res로 pipe + +## 6. 에러 처리 & 정합성 + +| 상황 | 처리 | +|------|------| +| S3 업로드 성공, DB INSERT 실패 | 업로드한 객체 보상 삭제(best-effort) 후 500 | +| DB 삭제 전 S3 삭제 실패 | 경고 로깅 후 DB 삭제 진행(고아 객체는 허용, 데이터 무결성 우선) | +| pgcrypto 미존재 | `crypto.randomUUID()` fallback | +| 파일 크기 초과 | multer 에러 → 413 | + +## 7. 의존성 변경 + +| 패키지 | 용도 | 비고 | +|--------|------|------| +| `multer` | multipart 업로드 파싱(메모리 스토리지) | 신규 추가 | +| `@aws-sdk/client-s3` | `GetObjectCommand`/`PutObjectCommand`/`DeleteObjectCommand` | 기존, 명령만 추가 사용 | + +> presigned 방식 미채택 → `@aws-sdk/s3-request-presigner` 추가하지 않음 (Plan 대비 변경점). +> 추가 후 `npm install`로 lock 갱신. + +## 8. Test Plan + +**L1 — API 엔드포인트 (서버 기동 시)** +- `GET /api/todos` → 200, `{ data: [] }` 형태 +- `POST /api/todos` (title만) → 201, data.id 존재 +- `POST /api/todos` (title 누락) → 400 +- `PATCH /api/todos/:id {done:true}` → 200, data.done=true +- `DELETE /api/todos/:id` → 204 +- `GET /api/todos/:bad-uuid/attachment` → 400/404 + +**L2 — UI 액션 (수동 또는 Playwright)** +- 페이지 로드 → 목록 렌더 +- 입력+추가 → 목록에 새 항목, 입력창 비워짐 +- 체크박스 토글 → done 반영 +- 삭제 → 항목 사라짐 +- 파일 첨부 추가 → 다운로드 링크 노출, 클릭 시 파일 받아짐 + +**L3 — E2E 시나리오** +- 첨부 포함 추가 → 다운로드 → 삭제까지 전체 흐름 무오류 +- 연결 점검: `npm run db:check`, `npm run minio:check` 통과 (SC-05) +- 컨테이너: `podman build` + `podman run --env-file .project-env` 동작 (SC-06) + +## 9. Success Criteria 매핑 + +| SC | Design 반영 위치 | +|----|------------------| +| SC-01 (CRUD UI) | §2 routes.todos.js + public/index.html, §4 API | +| SC-02 (업로드→S3+DB key) | §5 POST 흐름, §3 데이터 모델 | +| SC-03 (다운로드) | §5 GET attachment, 프록시 스트리밍 | +| SC-04 (삭제 시 S3도 삭제) | §5 DELETE 흐름, §6 정합성 | +| SC-05 (db/minio check) | §8 L3 | +| SC-06 (podman) | §8 L3 | + +## 10. 보안/규약 체크 + +- 비밀값: 전부 env(`config.js`) 경유, 코드/커밋 금지 (CLAUDE.md 준수). +- 외부 연결: db/s3/config 모듈만 경유. +- 입력 검증: title 길이/공백, uuid 형식, 파일 크기·(선택)타입. +- 내부 S3 키/호스트 비노출(프록시 + 응답에서 key 제외). + +## 11. Implementation Guide + +### 11.1 구현 순서 +1. `src/schema.js` — ensureSchema (pgcrypto 시도 + CREATE TABLE) +2. `src/s3.js` 확장 — putObject/getObjectStream/deleteObject export +3. `src/attachments.js` — 키 생성 규칙 + put/getStream/del +4. `src/todos.repo.js` — list/create/setDone/getById/remove +5. `src/routes.todos.js` — 6개 핸들러 + multer 미들웨어 + 검증 +6. `src/server.js` — express.json, static(public), ensureSchema 호출, 라우터 마운트 +7. `public/index.html` — fetch 기반 UI +8. `package.json` — multer 추가, `npm install` + +### 11.2 핵심 파일 +- `src/routes.todos.js` (가장 로직 밀집), `src/attachments.js`, `public/index.html` + +### 11.3 Session Guide (Module Map) + +| 모듈 키 | 범위 | 포함 파일 | 의존 | +|---------|------|-----------|------| +| `module-1` (데이터 계층) | schema + repo + s3/attachments | schema.js, s3.js(확장), attachments.js, todos.repo.js | 없음 | +| `module-2` (API 계층) | 라우트 + 서버 연결 + 의존성 | routes.todos.js, server.js, package.json | module-1 | +| `module-3` (UI) | 웹 프런트 | public/index.html | module-2 | + +**권장 세션 분할** +- 1세션에 전부 가능(소규모). 분할 시: `--scope module-1` → `--scope module-2` → `--scope module-3`. + +## 12. 다음 단계 + +`/pdca do todo-app` (전체) 또는 `/pdca do todo-app --scope module-1` (점진 구현). diff --git a/docs/03-analysis/todo-app.analysis.md b/docs/03-analysis/todo-app.analysis.md new file mode 100644 index 0000000..5ea7e63 --- /dev/null +++ b/docs/03-analysis/todo-app.analysis.md @@ -0,0 +1,100 @@ +# Analysis (Check) — todo-app + +> PDCA Phase: **Check** · Feature: `todo-app` · 작성일: 2026-06-15 +> 상위: [Plan](../01-plan/features/todo-app.plan.md) · [Design](../02-design/features/todo-app.design.md) + +## Context Anchor + +| 항목 | 내용 | +|------|------| +| WHY | 사내 샘플을 실제 쓰는 최소 To-Do 앱으로 — DB+S3 패턴 시연 | +| WHO | 로그인 없는 단일 공용 목록 | +| RISK | 첨부 처리, schema 마이그레이션, S3-DB 정합성 | +| SUCCESS | 브라우저 CRUD + 첨부 업/다운로드, db/minio check 통과 | +| SCOPE | IN: todo CRUD + 첨부 1개 + 웹UI + API / OUT: 인증·태그·검색 | + +## Match Rate + +| 축 | 점수 | 가중치 | 기여 | +|----|:----:|:------:|:----:| +| Structural | 100% | 0.15 | 15.0 | +| Functional | 96% | 0.25 | 24.0 | +| Contract | 100% | 0.25 | 25.0 | +| Runtime (L1) | 100% | 0.35 | 35.0 | +| **Overall** | **99%** | | | + +> 런타임 실행 공식 적용(서버 기동). 16/16 L1 테스트 통과, 서버 로그 에러 없음. + +## 1. Structural Match — 100% + +Design §2 모듈 구조 전부 존재, §4 라우트 6개(`GET /`, `GET/POST /api/todos`, `PATCH/DELETE /api/todos/:id`, `GET /api/todos/:id/attachment`) 모두 구현·마운트 확인. + +## 2. Functional Depth — 96% + +| 항목 | 결과 | +|------|------| +| 업로드 보상 삭제(§5,§6) | ✅ 업로드 실패 시 `repo.remove` | +| 삭제 시 S3 선삭제 후 DB(§6) | ✅ best-effort 로깅 후 진행 | +| pgcrypto fallback(§3) | ✅ 런타임에서 실제 발동 확인(`permission denied`→`randomUUID`) | +| 입력 검증(title/uuid/done) | ✅ | +| placeholder/미구현 | 없음 | + +**−4% 사유 (Minor)**: Design §5는 "단일 INSERT(첨부 키 포함)"로 기술됐으나, 구현은 "INSERT → setAttachment UPDATE" 2단계. 동작·정합성은 동일(오히려 id 기반 키 생성에 더 적합). 문서-구현 표현 불일치만 존재. + +## 3. API Contract — 100% (3자 검증) + +Design §4 ↔ `src/routes.todos.js` ↔ `public/index.html` fetch 호출 일치: +- 응답 envelope `{data}` / `{error}` ✅ +- `hasAttachment` 노출 + `attachment_key` 비노출 ✅ (런타임 L1-8 검증) +- 상태코드(201/400/404/413/204/200) 일치 ✅ +- 다운로드 `Content-Disposition` 헤더 ✅ (L1-10) + +## 4. Runtime Verification (L1) — 16/16 PASS + +| # | 검증 | 기대 | 결과 | +|---|------|:----:|:----:| +| 1 | GET / | 200 | ✅ | +| 2-3 | GET /api/todos + {data} | 200 | ✅ | +| 4 | POST title | 201 | ✅ | +| 5-6 | POST no title + {error} | 400 | ✅ | +| 7 | 첨부 hasAttachment | true | ✅ | +| 8 | attachment_key 비노출 | — | ✅ | +| 9-10 | 다운로드 + Content-Disposition | 200 | ✅ | +| 11 | PATCH done | 200 | ✅ | +| 12-14 | PATCH bad uuid/no done/404 | 400/400/404 | ✅ | +| 15 | DELETE | 204 | ✅ | +| 16 | 삭제 후 다운로드(cascade) | 404 | ✅ | + +## 5. Success Criteria 평가 + +| SC | 상태 | 근거 | +|----|:----:|------| +| SC-01 CRUD UI | ✅ Met | L1 + public/index.html | +| SC-02 업로드→S3+DB key | ✅ Met | L1-7, S3 객체 생성 | +| SC-03 다운로드 | ✅ Met | L1-9/10 내용 일치 | +| SC-04 삭제 시 S3도 삭제 | ✅ Met | L1-16 cascade | +| SC-05 db/minio check | ✅ Met | 둘 다 통과 | +| SC-06 podman | ⚠️ Partial | Dockerfile 무변경으로 빌드 가능하나 이 세션 미실행 | + +## 6. Decision Record 준수 검증 + +| 결정 | 준수 | +|------|:----:| +| Option C(repo/attachments/routes 분리) | ✅ | +| 서버 프록시 스트리밍(presigned 미사용) | ✅ `GetObjectCommand` pipe | +| pgcrypto + randomUUID fallback | ✅ 런타임 발동 | +| 변경점: multer 1.x→2.1.1 | ✅ 의도된 보안 대응, API 동일 | + +## 7. Gap 목록 + +| ID | Severity | 내용 | 조치 | +|----|:--------:|------|------| +| G1 | Minor | §5 단일 INSERT 서술 vs 2단계 구현 | 문서 동기화(선택) | +| G2 | Minor | SC-06 podman 빌드 미검증 | 컨테이너 빌드 1회 권장 | +| G3 | Minor | 파일 content-type 화이트리스트 미적용(NFR-03 일부) | OUT-of-scope 수준, 향후 | + +**Critical/Important 갭: 0건.** + +## 8. 결론 + +Overall **99%** (≥90% 목표 달성). Critical/Important 이슈 없음. Minor 3건은 모두 문서/범위 외/검증 절차 항목으로 코드 수정 불요. → iterate 없이 **QA 또는 report** 단계로 진행 가능. diff --git a/docs/04-report/todo-app.report.md b/docs/04-report/todo-app.report.md new file mode 100644 index 0000000..df95c70 --- /dev/null +++ b/docs/04-report/todo-app.report.md @@ -0,0 +1,75 @@ +# 완료 보고서 — todo-app (간단한 To-Do 앱) + +> PDCA Phase: **Report (completed)** · Feature: `todo-app` · 작성일: 2026-06-15 +> Match Rate: **99%** · 상위: [Plan](../01-plan/features/todo-app.plan.md) · [Design](../02-design/features/todo-app.design.md) · [Analysis](../03-analysis/todo-app.analysis.md) + +## Executive Summary + +| 관점 | 요약 | +|------|------| +| **Problem** | 할 일을 기록·관리할 화면/저장소 부재. 앱은 연결 확인 엔드포인트만 있었음. | +| **Solution** | Postgres에 todo CRUD, 항목별 첨부는 MinIO/S3에 저장하는 단일 공용 To-Do 웹 앱 구현. | +| **Function UX Effect** | 브라우저 추가/완료/삭제 + 첨부 업/다운로드, REST API 동시 제공. | +| **Core Value** | DB(구조화)+S3(바이너리) 결합의 가장 단순한 표준 패턴을 ~450줄로 완성. | + +### 1.3 Value Delivered (실측) + +| 관점 | 지표 | 결과 | +|------|------|------| +| 품질 | Match Rate | **99%** (목표 90%↑) | +| 검증 | L1 런타임 테스트 | **16/16 통과**, 서버 에러 0 | +| 연결 | db:check / minio:check | 둘 다 통과 | +| 범위 | Success Criteria | **5/6 Met**, 1 Partial(podman 미실행) | + +## 2. PDCA 여정 요약 + +| 단계 | 산출물 | 핵심 | +|------|--------|------| +| Plan | todo-app.plan.md | FR-01~08, 데이터 모델·API 초안, SC-01~06 | +| Design | todo-app.design.md | Option C(실용 균형) + 서버 프록시 스트리밍 선택 | +| Do | 코드 5신규/3수정 | schema/attachments/repo/routes/UI + s3·server·package | +| Check | todo-app.analysis.md | 99%, Critical 0, Minor 3 | + +## 3. 구현 내역 + +**신규**: `src/schema.js`, `src/attachments.js`, `src/todos.repo.js`, `src/routes.todos.js`, `public/index.html` +**수정**: `src/s3.js`(put/get/delete 추가), `src/server.js`(미들웨어·라우터·부트스트랩), `package.json`(multer 2.1.1) + +**API**: `GET /` · `GET/POST /api/todos` · `PATCH/DELETE /api/todos/:id` · `GET /api/todos/:id/attachment` + +## 4. Key Decisions & Outcomes + +| 결정 | 따랐는가 | 결과 | +|------|:-------:|------| +| Option C 아키텍처 | ✅ | repo/attachments/routes 분리, service 생략 — 적정 경계 | +| 서버 프록시 스트리밍(presigned 대신) | ✅ | 내부 MinIO 호스트 노출 없이 다운로드 동작, presigner 의존성 제거 | +| pgcrypto + randomUUID fallback | ✅ | **실환경에서 `permission denied` 발생 → fallback 자동 작동** (설계가 리스크 적중) | +| multer 1.x → 2.1.1 (구현 중 변경) | ✅ | npm 취약점 0, API 동일 | + +## 5. Success Criteria 최종 상태 + +| SC | 상태 | 근거 | +|----|:----:|------| +| SC-01 CRUD UI | ✅ Met | L1 + public/index.html | +| SC-02 업로드→S3+DB key | ✅ Met | L1-7 hasAttachment, S3 객체 생성 | +| SC-03 다운로드 | ✅ Met | L1-9/10 내용 일치 | +| SC-04 삭제 시 S3도 삭제 | ✅ Met | L1-16 cascade 404 | +| SC-05 db/minio check | ✅ Met | 둘 다 통과 | +| SC-06 podman | ⚠️ Not verified | Dockerfile 무변경, 이 세션 빌드 미실행 | + +**Success Rate: 5/6 Met (83%) + 1 미검증** + +## 6. 잔여 항목 (후속 권장) + +- **G2**: `podman build -t todo . && podman run --rm -p 3000:3000 --env-file .project-env todo`로 SC-06 확정. +- **G1**: Design §5 서술을 실제 INSERT→UPDATE 2단계로 문서 동기화(선택). +- **G3**: 첨부 content-type 화이트리스트(NFR-03 강화, 향후). +- 멀티유저/검색/마감일 등은 의도적 범위 외 — 차기 PDCA 사이클 후보. + +## 7. 배포 메모 + +코드/Dockerfile 변경만으로 배포 준비 완료(CLAUDE.md 규약). Coolify Environment에 `DATABASE_URL`, `S3_*` 설정 + git push → 자동 빌드. 비밀값은 전부 env 경유, 커밋 안 함. + +## 8. 결론 + +목표("DB와 S3를 이용한 간단한 To-Do 앱")를 99% 일치로 완성. 런타임 16/16 검증, 연결 점검 통과. Critical 이슈 없음. SC-06(podman)만 선택적 후속 검증 권장. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..fef80de --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1747 @@ +{ + "name": "sample", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sample", + "version": "0.1.0", + "dependencies": { + "@aws-sdk/client-s3": "^3.700.0", + "express": "^4.21.2", + "multer": "^2.1.1", + "pg": "^8.13.1" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/checksums": { + "version": "3.1000.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1000.5.tgz", + "integrity": "sha512-zOXUUnilC6lgCsQtp77p/QNPmRlTES9Xi6tlDwbR6kfC/kz5PCzZckgHWm5z+8DskdwuMAbFDq61x3zr10GEEQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.1068.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1068.0.tgz", + "integrity": "sha512-lFgaIpxZvloNbJvQ337YPdMXhzI2zJdDw13nATVGnkAGNoNPx4ksD84AQAcuW75hsaaMaIuNmXU9sSx6+FTirA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/credential-provider-node": "^3.972.55", + "@aws-sdk/middleware-flexible-checksums": "^3.974.30", + "@aws-sdk/middleware-sdk-s3": "^3.972.51", + "@aws-sdk/signature-v4-multi-region": "^3.996.34", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.974.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.20.tgz", + "integrity": "sha512-7sDi2B2N3mc3nf1nz6FyEx/FCrJ1N1QnBmraHHQNabFaeAh2IaOOLml48/rHOD1bICHgTRkbBgNTvUzEr5Z35g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.12", + "@aws-sdk/xml-builder": "^3.972.29", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.6", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.46.tgz", + "integrity": "sha512-+GPXVS2srMOlH74S+SmC1gVuP2TvUZ0siuC0onKO93q+udP+M72dmY8wJfVQ5CX9z/9X5A1HHwz5yRIGBtskvQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.48", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.48.tgz", + "integrity": "sha512-fA5loSdlocacRxyUXtpoHSMuk5rsIKRDzQYVMnMxjcmFeZshaJlJ8lymy/hYKji6sne/UmNGj5pxuEs6kq/Qcg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.53", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.53.tgz", + "integrity": "sha512-ZfdhIOR41q8TcWEnUac+gCOb+O2LBWdHLmjedXpXz4IEFW2ppNuFcm6p0sMTavpM+zD5TYfpH5Gp7guRyqSgsQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/credential-provider-env": "^3.972.46", + "@aws-sdk/credential-provider-http": "^3.972.48", + "@aws-sdk/credential-provider-login": "^3.972.52", + "@aws-sdk/credential-provider-process": "^3.972.46", + "@aws-sdk/credential-provider-sso": "^3.972.52", + "@aws-sdk/credential-provider-web-identity": "^3.972.52", + "@aws-sdk/nested-clients": "^3.997.20", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.52", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.52.tgz", + "integrity": "sha512-9hu2oR0qH7Fst5Tzdx+UWxm+w5zCXtErTLtOOW5hwwQc170CLwOeniRxyFY6s9mHfGEfC5zFukNBdKBwJR8mhQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/nested-clients": "^3.997.20", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.55", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.55.tgz", + "integrity": "sha512-zMGLa/dhESVqmCD7mmIFFKSwSFrJGScvCXcjvBZEVOOMauFS5JRQvLTMukFpMEFWiV6dTAlsen2ATDBulLPtbg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.46", + "@aws-sdk/credential-provider-http": "^3.972.48", + "@aws-sdk/credential-provider-ini": "^3.972.53", + "@aws-sdk/credential-provider-process": "^3.972.46", + "@aws-sdk/credential-provider-sso": "^3.972.52", + "@aws-sdk/credential-provider-web-identity": "^3.972.52", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.46.tgz", + "integrity": "sha512-VUoNFBIjWrUN8NbFiQiuxQEgFjvziAlBRPK+ddh27aj65gk0BYu6bLZnrdrNZwpW6vAihtSUtEMQ1PUJ32QRPA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.52", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.52.tgz", + "integrity": "sha512-nb2/n4o/HQf+FVpVbZe9vCTFngmuDoIsltMgLAtjixaKzvzhB4J8WSDFyWgnErgLHk55ctWH+I4PU+LIHhyffg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/nested-clients": "^3.997.20", + "@aws-sdk/token-providers": "3.1066.0", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.52", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.52.tgz", + "integrity": "sha512-lKj6aRSGbqLmpYmM24bY7a1Xmfcq2vkE3hv8CSPYfc1yCu0BPu/XEJ1L4Fm61MsU6ULLNSG8UGsffNoFUBjESA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/nested-clients": "^3.997.20", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.974.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.30.tgz", + "integrity": "sha512-OaIhub+3yTgfFWPzKO8OzOZFIMUoJaiS5v67y3spQg7SoULGoMx4jKVBbE+uhnzkiZXQ+rEDS0RqrK4/aD1yJw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/checksums": "^3.1000.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.51", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.51.tgz", + "integrity": "sha512-keQgcIUTcHL0Qn7guhsuLaxQU36r9norCrxgaPH4DNCwon4TPtXdI/UdYuycl9vj3Dlwc3YR1dfL3U+6iIwJ6w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/signature-v4-multi-region": "^3.996.34", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.20.tgz", + "integrity": "sha512-IYJuLpXp2DEILVQpQOy0PMpkftv0AHEOCn52o0atyOaumA0CdWQ3klPyXdViGYLbNpESsVFMVybvHUeZAuiGxA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/signature-v4-multi-region": "^3.996.34", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.34.tgz", + "integrity": "sha512-mx1L5qlumSOt/nKM3BFaHE2HVkWwz0i4Bw0pyYO42FfX/FeLlo8YI6csC0gSPprEk6fTIqI+CZN9RwUwKd5krQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.12", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1066.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1066.0.tgz", + "integrity": "sha512-UqEUJq7dqa44hneLDUcX7UJy95cg8YqEWyakRpvIPnrNS3Mq+UlQHgCDGu5pvwAPtlIW4qcYbvW6reG6++FyvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.20", + "@aws-sdk/nested-clients": "^3.997.20", + "@aws-sdk/types": "^3.973.12", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.12.tgz", + "integrity": "sha512-43ajd1NF0RMgX5k0hxCNUyEdrtFUsb2aHT2QvpktSC/2Eyb2Jr/JPVqdp0XIoaHWikZJq5tNWSLO6kB5q2eMCA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.7.tgz", + "integrity": "sha512-M0D6oIpohdNHjc7udzTHEQyot0+0iuA36jc2I9Hps+f/GtKi2HO/pyijQnCnNcwZqLB5+rtn81z3eZK/GyjAmA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.29.tgz", + "integrity": "sha512-fk0niuGFxfi8yIJuMVM4mhwObkiQSuwZFj3tAPrLVx64Pk3BkrEIpqjzHKY4hKoEBUD6Jg/S74Zj9jy+5F3DnQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.3", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@nodable/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@smithy/core": { + "version": "3.24.7", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.7.tgz", + "integrity": "sha512-KoUi4M1f3BG6kzN1FnCwL7oyFptTbyBJKjR6yhSib+JHRdUmM1o+VwsFtJ66NZCkCzVfJMWRHJNo0R0jznp0Pg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.9.tgz", + "integrity": "sha512-ZlfJ/4Fa3jYb+3eaohPfG9utX9HmdhFNcFtpoGAhUhdynAOmGXtmigbi7eEiONKM+ykHw8RwKuDEb85Lx7t7fA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.7", + "@smithy/types": "^4.14.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.4.7", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.7.tgz", + "integrity": "sha512-NslaM2ir0N2hisDmzXLstPaVINZheh8SokyOC++kzFPloZucL2R7Y7bS57mSzx/1Fc/fqmn7twjkeezTTrV0EA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.7", + "@smithy/types": "^4.14.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.8.tgz", + "integrity": "sha512-f+DbsWUwSbtMu1a/j8Y93KiU1SRg9nyzfjereqn1BJ33QOTUXxdlYvVXMhAYl1vuR1Kmna5aIJe09KSIfyFNYw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.7", + "@smithy/types": "^4.14.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.4.7", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.7.tgz", + "integrity": "sha512-LwQZazFayImv+IOm0S0enoLeUJwmAlhGC5O6YCcLWezyu08dF46GOxPOq35OpBIHkgd7OvNvBStIFwVNyrvoBw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.7", + "@smithy/types": "^4.14.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.14.4", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.4.tgz", + "integrity": "sha512-B2S9+UGm1+/pHkcx3ZoLVX1a+pmSk8rqxRR+ZsNqZaJ5q9FWX9AFGQVM4qG5+OBeQUZVy99HY8HqW8gK/wgXzQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/anynum": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.0.tgz", + "integrity": "sha512-xjR9/zBVnUOP6ztMIIgShjsxui80nQUQH+5xJnvrYLs+90bF25/KJqaAi8mk+B4RDtX1Nspi6fmp4YTEts8SfA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", + "license": "MIT" + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/multer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.1.1.tgz", + "integrity": "sha512-mo+QTzKlx8R7E5ylSXxWzGoXoZbOsRMpyitcht8By2KHvMbf3tjwosZ/Mu/XYU6UuJ3VZnODIrak5ZrPiPyB6A==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "type-is": "^1.6.18" + }, + "engines": { + "node": ">= 10.16.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.21.0.tgz", + "integrity": "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.13.0", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.14.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", + "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.13.0.tgz", + "integrity": "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.14.0.tgz", + "integrity": "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strnum": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.4.0.tgz", + "integrity": "sha512-sHrVyWWdq28RbhjuJdZsA1SnGRJV6NiXbk6AXBxDOsgAcA+lmpUZCYjOdLBxkXMwis6RRe7dlZt4VlIWFVzkmg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "anynum": "^1.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5faec33 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "name": "sample", + "version": "0.1.0", + "description": "사내 Node 샘플 — podman 로컬개발 + Coolify(Dockerfile) 배포", + "type": "module", + "main": "src/server.js", + "scripts": { + "dev": "node --watch src/server.js", + "start": "node src/server.js", + "db:check": "node scripts/check-db.js", + "minio:check": "node scripts/check-minio.js" + }, + "engines": { + "node": ">=22" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.700.0", + "express": "^4.21.2", + "multer": "^2.1.1", + "pg": "^8.13.1" + } +} diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..a3a4859 --- /dev/null +++ b/public/index.html @@ -0,0 +1,132 @@ + + + + + + + To-Do + + + +
+

📝 To-Do

+ +
+ + + +
+ +
    +
    +
    + + + + diff --git a/scripts/check-db.js b/scripts/check-db.js new file mode 100644 index 0000000..1ad376d --- /dev/null +++ b/scripts/check-db.js @@ -0,0 +1,11 @@ +// DB 연결 빠른 확인: `npm run db:check` +import { pingDb, pool } from "../src/db.js"; + +try { + console.log("DB OK:", await pingDb()); +} catch (e) { + console.error("DB FAIL:", e.message); + process.exitCode = 1; +} finally { + await pool.end(); +} diff --git a/scripts/check-minio.js b/scripts/check-minio.js new file mode 100644 index 0000000..d809fb4 --- /dev/null +++ b/scripts/check-minio.js @@ -0,0 +1,10 @@ +// MinIO(S3) 연결 빠른 확인: `npm run minio:check` +// 먼저 ~/.config 또는 .env 에서 S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY 를 채워야 함. +import { pingS3 } from "../src/s3.js"; + +try { + console.log("S3 OK:", await pingS3()); +} catch (e) { + console.error("S3 FAIL:", e.message); + process.exitCode = 1; +} diff --git a/src/attachments.js b/src/attachments.js new file mode 100644 index 0000000..e8b3b67 --- /dev/null +++ b/src/attachments.js @@ -0,0 +1,31 @@ +// Design Ref: §2 attachments — S3 첨부 처리 (키 규칙 + put/getStream/del) +// 키 규칙: todos/{todoId}/{원본파일명}. 외부 연결은 s3.js 경유. +import { putObject, getObject, deleteObject } from "./s3.js"; + +// 파일명에서 경로 구분자/제어문자를 제거해 키 안전성 확보. +function sanitizeFilename(name) { + const base = String(name || "file").split(/[\\/]/).pop(); + const cleaned = base.replace(/[\x00-\x1f]/g, "").trim(); + return cleaned || "file"; +} + +export function buildKey(todoId, filename) { + return `todos/${todoId}/${sanitizeFilename(filename)}`; +} + +// Plan SC: SC-02 — 업로드 후 키를 반환(호출측이 DB에 기록). +export async function upload(todoId, filename, buffer, contentType) { + const key = buildKey(todoId, filename); + await putObject(key, buffer, contentType); + return key; +} + +// Plan SC: SC-03 — 다운로드 스트림 반환(서버 프록시 스트리밍). +export async function getStream(key) { + return getObject(key); +} + +// Plan SC: SC-04 — 삭제. best-effort(실패는 호출측에서 로깅 후 진행). +export async function remove(key) { + await deleteObject(key); +} diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..daf53e2 --- /dev/null +++ b/src/config.js @@ -0,0 +1,26 @@ +// 환경설정 — 모든 외부 연결정보는 환경변수에서만 읽는다(코드에 비밀값 금지). +// +// 값의 출처: 프로젝트 폴더의 .project-env (DB 본인 schema + S3). 프로젝트 폴더에 cd 하면 +// 로그인 셸이 자동 export → 여기 process.env 로 그대로 들어온다(별도 dotenv 불필요). +// MinIO(S3): 사내 오브젝트 스토리지. endpoint는 API 호스트(minio.bokdev.in), +// 콘솔(minioc.bokdev.in)이 아님에 주의. +// +// Coolify 배포 시에는 Coolify의 Environment Variables 화면에서 같은 키들을 채운다. + +export const config = { + port: parseInt(process.env.PORT || "3000", 10), + + // Postgres — .project-env 의 DATABASE_URL. Coolify에선 본인 DB 접속정보를 넣을 것. + databaseUrl: process.env.DATABASE_URL || "", + + // MinIO / S3 호환 + s3: { + endpoint: process.env.S3_ENDPOINT || "https://minio.bokdev.in", + region: process.env.S3_REGION || "us-east-1", + bucket: process.env.S3_BUCKET || "coolify-user-data", + accessKeyId: process.env.S3_ACCESS_KEY_ID || "", + secretAccessKey: process.env.S3_SECRET_ACCESS_KEY || "", + // MinIO는 path-style 필요 + forcePathStyle: true, + }, +}; diff --git a/src/db.js b/src/db.js new file mode 100644 index 0000000..0c5aec4 --- /dev/null +++ b/src/db.js @@ -0,0 +1,15 @@ +// Postgres 연결 풀. DATABASE_URL 한 줄로 접속(직원 전용 schema가 search_path에 고정됨). +import pg from "pg"; +import { config } from "./config.js"; + +export const pool = new pg.Pool({ + connectionString: config.databaseUrl, + max: 5, +}); + +export async function pingDb() { + const { rows } = await pool.query( + "select current_user as user, current_schema as schema, now() as now" + ); + return rows[0]; +} diff --git a/src/routes.todos.js b/src/routes.todos.js new file mode 100644 index 0000000..21bb095 --- /dev/null +++ b/src/routes.todos.js @@ -0,0 +1,145 @@ +// Design Ref: §4 API 계약, §5 처리 흐름 — todo REST 라우터. +// repo + attachments 를 조합(별도 service 계층 없음 = Option C 실용 균형). +import { Router } from "express"; +import multer from "multer"; +import * as repo from "./todos.repo.js"; +import * as attachments from "./attachments.js"; + +// Design Ref: §4 — 업로드 10MB 제한, 메모리 스토리지(버퍼로 S3 직전달). +const upload = multer({ + storage: multer.memoryStorage(), + limits: { fileSize: 10 * 1024 * 1024 }, +}); + +const UUID_RE = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +export const todosRouter = Router(); + +// GET /api/todos — 목록(최신순) +todosRouter.get("/", async (_req, res) => { + try { + res.json({ data: await repo.list() }); + } catch (e) { + res.status(500).json({ error: String(e.message || e) }); + } +}); + +// POST /api/todos — 추가 (multipart: title 필수, file 선택). Plan SC: SC-02 +todosRouter.post("/", upload.single("file"), async (req, res) => { + try { + const title = (req.body?.title || "").trim(); + if (!title) return res.status(400).json({ error: "title is required" }); + + // 첨부 없으면 단순 생성 + if (!req.file) { + return res.status(201).json({ data: await repo.create({ title }) }); + } + + // Design Ref: §5 — 키는 todo id 기준. 선 INSERT로 id 확보 후 업로드, + // 업로드 실패 시 생성된 row 보상 삭제(정합성 §6). + const created = await repo.create({ title }); + try { + const key = await attachments.upload( + created.id, + req.file.originalname, + req.file.buffer, + req.file.mimetype + ); + const updated = await repo.setAttachment(created.id, { + key, + name: req.file.originalname, + type: req.file.mimetype, + }); + return res.status(201).json({ data: updated }); + } catch (uploadErr) { + await repo.remove(created.id).catch(() => {}); + throw uploadErr; + } + } catch (e) { + res.status(500).json({ error: String(e.message || e) }); + } +}); + +// PATCH /api/todos/:id — done 토글 +todosRouter.patch("/:id", async (req, res) => { + try { + const { id } = req.params; + if (!UUID_RE.test(id)) return res.status(400).json({ error: "invalid id" }); + if (typeof req.body?.done !== "boolean") { + return res.status(400).json({ error: "done(boolean) is required" }); + } + const updated = await repo.setDone(id, req.body.done); + if (!updated) return res.status(404).json({ error: "not found" }); + res.json({ data: updated }); + } catch (e) { + res.status(500).json({ error: String(e.message || e) }); + } +}); + +// DELETE /api/todos/:id — 삭제(+ S3 첨부 삭제). Plan SC: SC-04 +todosRouter.delete("/:id", async (req, res) => { + try { + const { id } = req.params; + if (!UUID_RE.test(id)) return res.status(400).json({ error: "invalid id" }); + const row = await repo.getRaw(id); + if (!row) return res.status(404).json({ error: "not found" }); + + // Design Ref: §6 — S3 먼저 시도(실패는 로깅 후 진행), 그다음 DB 삭제. + if (row.attachment_key) { + try { + await attachments.remove(row.attachment_key); + } catch (delErr) { + console.warn( + `[todos] S3 첨부 삭제 실패(고아 객체 허용): ${row.attachment_key} — ${delErr.message || delErr}` + ); + } + } + await repo.remove(id); + res.status(204).end(); + } catch (e) { + res.status(500).json({ error: String(e.message || e) }); + } +}); + +// GET /api/todos/:id/attachment — 첨부 다운로드(서버 프록시 스트리밍). Plan SC: SC-03 +todosRouter.get("/:id/attachment", async (req, res) => { + try { + const { id } = req.params; + if (!UUID_RE.test(id)) return res.status(400).json({ error: "invalid id" }); + const row = await repo.getRaw(id); + if (!row || !row.attachment_key) { + return res.status(404).json({ error: "no attachment" }); + } + const stream = await attachments.getStream(row.attachment_key); + res.setHeader( + "Content-Type", + row.attachment_type || "application/octet-stream" + ); + res.setHeader( + "Content-Disposition", + `attachment; filename="${encodeURIComponent(row.attachment_name || "file")}"` + ); + stream.on("error", (err) => { + console.error(`[todos] stream error: ${err.message || err}`); + if (!res.headersSent) res.status(500).end(); + else res.destroy(err); + }); + stream.pipe(res); + } catch (e) { + res.status(500).json({ error: String(e.message || e) }); + } +}); + +// Design Ref: §4 — multer 등 미들웨어 에러는 next(err)로 흐르므로 라우터 레벨에서 처리. +// 업로드 크기 초과 → 413, 그 외 → 400/500. +todosRouter.use((err, _req, res, _next) => { + if (err?.code === "LIMIT_FILE_SIZE") { + return res.status(413).json({ error: "file too large (max 10MB)" }); + } + if (err?.name === "MulterError") { + return res.status(400).json({ error: err.message }); + } + console.error(`[todos] unhandled: ${err?.message || err}`); + res.status(500).json({ error: "internal error" }); +}); diff --git a/src/s3.js b/src/s3.js new file mode 100644 index 0000000..d7b967c --- /dev/null +++ b/src/s3.js @@ -0,0 +1,56 @@ +// MinIO(S3 호환) 클라이언트. 자격증명은 env에서만 읽는다. +import { + S3Client, + ListObjectsV2Command, + HeadBucketCommand, + PutObjectCommand, + GetObjectCommand, + DeleteObjectCommand, +} from "@aws-sdk/client-s3"; +import { config } from "./config.js"; + +export const s3 = new S3Client({ + endpoint: config.s3.endpoint, + region: config.s3.region, + forcePathStyle: config.s3.forcePathStyle, + credentials: { + accessKeyId: config.s3.accessKeyId, + secretAccessKey: config.s3.secretAccessKey, + }, +}); + +export async function pingS3() { + await s3.send(new HeadBucketCommand({ Bucket: config.s3.bucket })); + const out = await s3.send( + new ListObjectsV2Command({ Bucket: config.s3.bucket, MaxKeys: 5 }) + ); + return { + bucket: config.s3.bucket, + sampleKeys: (out.Contents || []).map((o) => o.Key), + }; +} + +// Design Ref: §2,§7 — 첨부 처리에 필요한 저수준 S3 명령 래퍼. +// 도메인 로직(키 규칙 등)은 attachments.js 가 담당하고, 여기선 버킷 고정 I/O만. +const Bucket = config.s3.bucket; + +export async function putObject(key, body, contentType) { + await s3.send( + new PutObjectCommand({ + Bucket, + Key: key, + Body: body, + ContentType: contentType, + }) + ); +} + +// 다운로드는 서버 프록시 스트리밍 방식(presigned 미사용). Body 는 Node Readable. +export async function getObject(key) { + const out = await s3.send(new GetObjectCommand({ Bucket, Key: key })); + return out.Body; +} + +export async function deleteObject(key) { + await s3.send(new DeleteObjectCommand({ Bucket, Key: key })); +} diff --git a/src/schema.js b/src/schema.js new file mode 100644 index 0000000..5920555 --- /dev/null +++ b/src/schema.js @@ -0,0 +1,48 @@ +// Design Ref: §3 데이터 모델 — todo 테이블 멱등 생성 + pgcrypto fallback +// 앱 시작 시 1회 호출(server.js)하여 스키마를 보장한다. 외부 연결은 db.js pool 경유. +import { randomUUID } from "node:crypto"; +import { pool } from "./db.js"; + +// pgcrypto(gen_random_uuid)가 가용한지 여부. ensureSchema 에서 결정되어 +// repo 의 id 생성 전략(DB default vs app-side randomUUID)에 쓰인다. +let pgcryptoAvailable = false; + +export function hasPgcrypto() { + return pgcryptoAvailable; +} + +// 앱에서 id를 만들 때 사용(pgcrypto 미가용 fallback). Plan SC: SC-02 +export function newId() { + return randomUUID(); +} + +export async function ensureSchema() { + // pgcrypto 확장 시도 — 권한 부족 등으로 실패하면 무시하고 app-side UUID로 fallback + try { + await pool.query("CREATE EXTENSION IF NOT EXISTS pgcrypto"); + pgcryptoAvailable = true; + } catch (e) { + pgcryptoAvailable = false; + console.warn( + `[schema] pgcrypto 미가용 — app-side UUID로 진행: ${e.message || e}` + ); + } + + // gen_random_uuid() default 는 pgcrypto 가 있을 때만 건다. + const idDefault = pgcryptoAvailable ? " DEFAULT gen_random_uuid()" : ""; + + await pool.query(` + CREATE TABLE IF NOT EXISTS todo ( + id uuid PRIMARY KEY${idDefault}, + title text NOT NULL, + done boolean NOT NULL DEFAULT false, + attachment_key text, + attachment_name text, + attachment_type text, + created_at timestamptz NOT NULL DEFAULT now() + ) + `); + await pool.query( + "CREATE INDEX IF NOT EXISTS idx_todo_created_at ON todo (created_at DESC)" + ); +} diff --git a/src/server.js b/src/server.js new file mode 100644 index 0000000..bfd67ac --- /dev/null +++ b/src/server.js @@ -0,0 +1,46 @@ +// 최소 Express 서버 — /healthz, /db, /s3 연결 확인 + To-Do 앱(/, /api/todos). +import express from "express"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { config } from "./config.js"; +import { pingDb } from "./db.js"; +import { pingS3 } from "./s3.js"; +import { ensureSchema } from "./schema.js"; // Design Ref: §3 schema bootstrap +import { todosRouter } from "./routes.todos.js"; + +const app = express(); +const __dirname = dirname(fileURLToPath(import.meta.url)); + +app.use(express.json()); +// Design Ref: §2 — 정적 웹 UI(public/index.html). "/" 는 index.html 자동 서빙. +app.use(express.static(join(__dirname, "..", "public"))); + +// Design Ref: §4 — To-Do REST API +app.use("/api/todos", todosRouter); + +app.get("/healthz", (_req, res) => res.json({ ok: true })); + +app.get("/db", async (_req, res) => { + try { + res.json({ ok: true, ...(await pingDb()) }); + } catch (e) { + res.status(500).json({ ok: false, error: String(e.message || e) }); + } +}); + +app.get("/s3", async (_req, res) => { + try { + res.json({ ok: true, ...(await pingS3()) }); + } catch (e) { + res.status(500).json({ ok: false, error: String(e.message || e) }); + } +}); + +// Plan SC: SC-01 — 스키마 보장 후 서버 기동. 실패해도 헬스 엔드포인트는 떠야 하므로 경고만. +ensureSchema() + .then(() => console.log("[schema] todo table ready")) + .catch((e) => console.error(`[schema] ensureSchema failed: ${e.message || e}`)); + +app.listen(config.port, "0.0.0.0", () => { + console.log(`todo-app listening on :${config.port}`); +}); diff --git a/src/todos.repo.js b/src/todos.repo.js new file mode 100644 index 0000000..f3b142f --- /dev/null +++ b/src/todos.repo.js @@ -0,0 +1,71 @@ +// Design Ref: §2 repo — todo DB CRUD. 외부 연결은 db.js pool 경유. +import { pool } from "./db.js"; +import { hasPgcrypto, newId } from "./schema.js"; + +// Design Ref: §4 — 응답에서 내부 S3 key 는 제외하고 hasAttachment 만 노출. +function toDto(row) { + if (!row) return null; + return { + id: row.id, + title: row.title, + done: row.done, + attachment_name: row.attachment_name, + attachment_type: row.attachment_type, + hasAttachment: row.attachment_key != null, + created_at: row.created_at, + }; +} + +export async function list() { + const { rows } = await pool.query( + "SELECT * FROM todo ORDER BY created_at DESC" + ); + return rows.map(toDto); +} + +// Plan SC: SC-02 — pgcrypto 가용 시 DB default, 아니면 app-side UUID. +export async function create({ title, attachmentKey, attachmentName, attachmentType }) { + if (hasPgcrypto()) { + const { rows } = await pool.query( + `INSERT INTO todo (title, attachment_key, attachment_name, attachment_type) + VALUES ($1, $2, $3, $4) RETURNING *`, + [title, attachmentKey ?? null, attachmentName ?? null, attachmentType ?? null] + ); + return toDto(rows[0]); + } + const { rows } = await pool.query( + `INSERT INTO todo (id, title, attachment_key, attachment_name, attachment_type) + VALUES ($1, $2, $3, $4, $5) RETURNING *`, + [newId(), title, attachmentKey ?? null, attachmentName ?? null, attachmentType ?? null] + ); + return toDto(rows[0]); +} + +// 첨부 업로드 후 키/메타를 기존 todo row 에 반영. Plan SC: SC-02 +export async function setAttachment(id, { key, name, type }) { + const { rows } = await pool.query( + `UPDATE todo SET attachment_key = $2, attachment_name = $3, attachment_type = $4 + WHERE id = $1 RETURNING *`, + [id, key, name, type] + ); + return toDto(rows[0]); +} + +export async function setDone(id, done) { + const { rows } = await pool.query( + "UPDATE todo SET done = $2 WHERE id = $1 RETURNING *", + [id, done] + ); + return toDto(rows[0]); // 없으면 null +} + +// 내부용: 삭제/다운로드 시 attachment_key 가 필요하므로 raw row 반환. +export async function getRaw(id) { + const { rows } = await pool.query("SELECT * FROM todo WHERE id = $1", [id]); + return rows[0] ?? null; +} + +export async function remove(id) { + const { rowCount } = await pool.query("DELETE FROM todo WHERE id = $1", [id]); + return rowCount > 0; +}