From 6ef64a9e82a97d01312a720877f0e7bffeb892b2 Mon Sep 17 00:00:00 2001 From: Arin0303 Date: Sun, 25 Jan 2026 23:33:05 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat:=20=EC=86=8C=EC=BC=93=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0,=20=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=A0=84=EC=86=A1,?= =?UTF-8?q?=20=EB=A9=94=EC=84=B8=EC=A7=80=20=EC=88=98=EC=8B=A0=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 + pnpm-lock.yaml | 429 +++++++++++++++++++++++ src/chat/dtos/chat.dto.ts | 2 + src/chat/repositories/chat.repository.ts | 39 ++- src/chat/services/chat.service.ts | 27 ++ src/index.ts | 10 +- src/socket/server.ts | 131 +++++++ src/socket/test-client.ts | 29 ++ src/utils/map.ts | 8 + 9 files changed, 676 insertions(+), 2 deletions(-) create mode 100644 src/socket/server.ts create mode 100644 src/socket/test-client.ts create mode 100644 src/utils/map.ts diff --git a/package.json b/package.json index 792135e..c42c20f 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,8 @@ "passport-naver-v2": "^2.0.8", "reflect-metadata": "^0.2.2", "save-dev": "0.0.1-security", + "socket.io": "^4.8.3", + "socket.io-client": "^4.8.3", "swagger": "^0.7.5", "swagger-autogen": "^2.23.7", "swagger-jsdoc": "^6.2.8", @@ -74,6 +76,7 @@ "ts-node": "^10.9.2", "ts-node-dev": "^2.0.0", "tsoa": "^6.6.0", + "tsx": "^4.21.0", "typescript": "^5.8.3" }, "prisma": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ecf7c82..acb7858 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,6 +86,12 @@ importers: save-dev: specifier: 0.0.1-security version: 0.0.1-security + socket.io: + specifier: ^4.8.3 + version: 4.8.3 + socket.io-client: + specifier: ^4.8.3 + version: 4.8.3 swagger: specifier: ^0.7.5 version: 0.7.5 @@ -171,6 +177,9 @@ importers: tsoa: specifier: ^6.6.0 version: 6.6.0 + tsx: + specifier: ^4.21.0 + version: 4.21.0 typescript: specifier: ^5.8.3 version: 5.9.3 @@ -363,6 +372,162 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@hapi/accept@6.0.3': resolution: {integrity: sha512-p72f9k56EuF0n3MwlBNThyVE5PXX40g+aQh+C/xbKrfzahM2Oispv3AXmOIU51t3j77zay1qrX7IIziZXspMlw==} @@ -731,6 +896,9 @@ packages: resolution: {integrity: sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==} engines: {node: '>=18.0.0'} + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -1044,6 +1212,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + base64url@3.0.1: resolution: {integrity: sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==} engines: {node: '>=6.0.0'} @@ -1560,6 +1732,17 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + engine.io-client@6.6.4: + resolution: {integrity: sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.5: + resolution: {integrity: sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==} + engines: {node: '>=10.2.0'} + es-abstract@1.24.0: resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} engines: {node: '>= 0.4'} @@ -1584,6 +1767,11 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -1800,6 +1988,9 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + get-value@2.0.6: resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} engines: {node: '>=0.10.0'} @@ -2965,6 +3156,9 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve-url@0.2.1: resolution: {integrity: sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==} deprecated: https://github.com/lydell/resolve-url#deprecated @@ -3137,6 +3331,21 @@ packages: resolution: {integrity: sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==} engines: {node: '>=0.10.0'} + socket.io-adapter@2.5.6: + resolution: {integrity: sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==} + + socket.io-client@4.8.3: + resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.5: + resolution: {integrity: sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.3: + resolution: {integrity: sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==} + engines: {node: '>=10.2.0'} + source-map-resolve@0.5.3: resolution: {integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==} deprecated: See https://github.com/lydell/source-map-resolve#deprecated @@ -3426,6 +3635,11 @@ packages: engines: {node: '>=18.0.0', yarn: '>=1.9.4'} hasBin: true + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + type-is@1.6.18: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} @@ -3612,10 +3826,26 @@ packages: write-file-atomic@2.4.3: resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xdg-basedir@3.0.0: resolution: {integrity: sha512-1Dly4xqlulvPD3fZUQJLY+FUIeqN3N2MM3uqe4rCJftAvOjFa3jFGfctOgluGx4ahPbUCsZkmJILiP0Vi4T6lQ==} engines: {node: '>=4'} + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -4214,6 +4444,84 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + '@hapi/accept@6.0.3': dependencies: '@hapi/boom': 10.0.1 @@ -4781,6 +5089,8 @@ snapshots: dependencies: tslib: 2.8.1 + '@socket.io/component-emitter@3.1.2': {} + '@standard-schema/spec@1.0.0': {} '@tsconfig/node10@1.0.11': {} @@ -5135,6 +5445,8 @@ snapshots: balanced-match@1.0.2: {} + base64id@2.0.0: {} + base64url@3.0.1: {} base@0.11.2: @@ -5704,6 +6016,36 @@ snapshots: encodeurl@2.0.0: {} + engine.io-client@6.6.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3(supports-color@5.5.0) + engine.io-parser: 5.2.3 + ws: 8.18.3 + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + + engine.io@6.6.5: + dependencies: + '@types/cors': 2.8.19 + '@types/node': 24.10.0 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.5 + debug: 4.4.3(supports-color@5.5.0) + engine.io-parser: 5.2.3 + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + es-abstract@1.24.0: dependencies: array-buffer-byte-length: 1.0.2 @@ -5782,6 +6124,35 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -6104,6 +6475,10 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + get-value@2.0.6: {} giget@2.0.0: @@ -7373,6 +7748,8 @@ snapshots: require-directory@2.1.1: {} + resolve-pkg-maps@1.0.0: {} + resolve-url@0.2.1: {} resolve@1.22.11: @@ -7609,6 +7986,47 @@ snapshots: transitivePeerDependencies: - supports-color + socket.io-adapter@2.5.6: + dependencies: + debug: 4.4.3(supports-color@5.5.0) + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-client@4.8.3: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3(supports-color@5.5.0) + engine.io-client: 6.6.4 + socket.io-parser: 4.2.5 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.5: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.4.3(supports-color@5.5.0) + transitivePeerDependencies: + - supports-color + + socket.io@4.8.3: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.4.3(supports-color@5.5.0) + engine.io: 6.6.5 + socket.io-adapter: 2.5.6 + socket.io-parser: 4.2.5 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + source-map-resolve@0.5.3: dependencies: atob: 2.1.2 @@ -7971,6 +8389,13 @@ snapshots: transitivePeerDependencies: - supports-color + tsx@4.21.0: + dependencies: + esbuild: 0.27.2 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + type-is@1.6.18: dependencies: media-typer: 0.3.0 @@ -8194,8 +8619,12 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 + ws@8.18.3: {} + xdg-basedir@3.0.0: {} + xmlhttprequest-ssl@2.1.2: {} + xtend@4.0.2: {} y18n@5.0.8: {} diff --git a/src/chat/dtos/chat.dto.ts b/src/chat/dtos/chat.dto.ts index b3adeaa..02b24ed 100644 --- a/src/chat/dtos/chat.dto.ts +++ b/src/chat/dtos/chat.dto.ts @@ -1,3 +1,5 @@ +import { AttachmentType } from "@prisma/client"; + export type ChatFilterType = "all" | "unread" | "pinned"; // == 채팅방 생성 diff --git a/src/chat/repositories/chat.repository.ts b/src/chat/repositories/chat.repository.ts index f032afd..81a9629 100644 --- a/src/chat/repositories/chat.repository.ts +++ b/src/chat/repositories/chat.repository.ts @@ -1,5 +1,5 @@ import prisma from "../../config/prisma"; -import { Prisma } from "@prisma/client"; +import { Prisma, AttachmentType } from "@prisma/client"; import { ChatFilterType } from "../dtos/chat.dto"; export class ChatRepository { @@ -253,5 +253,42 @@ export class ChatRepository { }, }); } + + // == 메세지 저장 + async saveMessage( + roomId: number, + senderId: number, + content: string, + files: { url: string; contentType: AttachmentType; name: string; size: number }[] + ) { + + return prisma.chatMessage.create({ + data: { + room_id: roomId, + sender_id: senderId, + content: content, + attachments: { + create: files?.map((f) => ({ + url: f.url, + name: f.name, + size: f.size, + type: f.contentType + })) || [] + }, + }, + include: { + user: { + select: { + user_id: true, + nickname: true, + profileImage: true, + }, + }, + attachments: true, + }, + }); + } + + } diff --git a/src/chat/services/chat.service.ts b/src/chat/services/chat.service.ts index 214d134..e27b19c 100644 --- a/src/chat/services/chat.service.ts +++ b/src/chat/services/chat.service.ts @@ -8,6 +8,7 @@ import { TogglePinResponseDto, } from "../dtos/chat.dto"; import { getPresignedUrl } from "../../middlewares/s3.util"; +import { mapMimeTypeToEnum } from "../../utils/map"; export class ChatService { constructor(private readonly chatRepo: ChatRepository) {} @@ -151,6 +152,32 @@ export class ChatService { const togglePinned = (await this.chatRepo.togglePinChatRoom(roomId, userId, isPinned)).is_pinned return {is_pinned: togglePinned}; } + + // == 읽음 처리 + async resetUnreadCountService(roomId: number, userId: number): Promise { + await this.chatRepo.resetUnreadCount(roomId, userId); + } + + // == 메세지 저장 + async saveMessageService( + params: { + roomId: number; + senderId: number; + content: string; + files: { key: string; contentType: string; name: string; size: number }[] + } + ) { + const { roomId, senderId, content, files } = params; + const s3BaseUrl = `https://${process.env.S3_BUCKET}.s3.${process.env.S3_REGION}.amazonaws.com/`; + const formattedFiles = files.map((f) => ({ + url: `${s3BaseUrl}${f.key}`, // URL 생성 + name: f.name, + size: f.size, + contentType: mapMimeTypeToEnum(f.contentType) + })) + + return this.chatRepo.saveMessage(roomId, senderId, content, formattedFiles); + } } export const chatService = new ChatService(new ChatRepository()); diff --git a/src/index.ts b/src/index.ts index 9bfcf1f..c875035 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,6 @@ import express, { ErrorRequestHandler } from "express"; +import http from "http"; +import { Server } from "socket.io" import { responseHandler } from "./middlewares/responseHandler"; import { errorHandler } from "./middlewares/errorHandler"; import "reflect-metadata"; @@ -31,9 +33,15 @@ import signupRouter from "./signup/routes/signup.route" import signinRouter from "./signin/routes/signin.route"; import passwordRouter from "./password/routes/password.route"; import chatRouter from "./chat/routes/chat.route"; +import { initSocket } from "./socket/server"; import morgan = require('morgan'); const PORT = 3000; const app = express(); + +// Express 앱을 http 서버로 감싸기 +const server = http.createServer(app); + +initSocket(server) // 1. 응답 핸들러(json 파서보다 위에) app.use(responseHandler); app.use((req, res, next) => { @@ -200,6 +208,6 @@ app.get('/health', (req, res) => { app.use(morgan('dev')); // 사용자 요청 로그 출력 -app.listen(PORT, "0.0.0.0", () => { +server.listen(PORT, "0.0.0.0", () => { console.log(`Server is running at http://localhost:${PORT}`); }); diff --git a/src/socket/server.ts b/src/socket/server.ts new file mode 100644 index 0000000..38e7a13 --- /dev/null +++ b/src/socket/server.ts @@ -0,0 +1,131 @@ +import { chatService } from "../chat/services/chat.service"; +import { AppError } from "../errors/AppError"; +import { Server as SocketIOServer, Socket } from "socket.io"; +import { Server as HttpServer } from "http"; +import jwt from "jsonwebtoken"; +import prisma from "../config/prisma"; +import e from "express"; + +const setupSocketEvents = (io: SocketIOServer, socket: Socket) => { + const userId = socket.data.userId; // 미들웨어에서 설정한 id + + // 방 입장 + socket.on("joinRoom", async (data, ack) => { + + try { + console.log("data:", data) + const roomId:number = data.room_id; + + if (!roomId) { + return ack({ok: false, message: "입력 필드에 room_id가 없습니다."}) + } + + socket.join(String(roomId)); + + ack(({ok: true, room_id: roomId})); + + console.log(`[on]-joinRoom 성공: 유저 ${userId}님이 ${roomId}번 방에 입장했습니다.`); + + // 읽음 처리 + await chatService.resetUnreadCountService(Number(roomId), userId); + + } catch (err: any){ + console.error(err); + ack?.({ok: false, message: err.message}); + } + }); + + // 메세지 전송 + socket.on("sendMessage", async (data, ack) => { + try { + const { room_id, content, files } = data; + + // 메세지를 db에 저장 + const savedMessage = await chatService.saveMessageService({ + roomId: Number(room_id), + senderId: userId, + content: content, + files: files || [] + }); + console.log(`[on]- sendMessage 성공: 유저 ${userId}님이 "${content}"를 보냈습니다.`); + // 같은 방에 있는 전체 참여자가 메세지 수신 + io.to(room_id.toString()).emit("receiveMessage", savedMessage); + + ack?.({ok: true, message: savedMessage}); + + } catch (err: any) { + console.error("Error:", err); + ack?.({ok: false, message: err.message}); + } + }); + + // 방 나가기 (뒤로가기) + socket.on("leaveRoom", (roomId: string) => { + socket.leave(roomId); + console.log(`유저 ${userId}님이 방 ${roomId}을 나갔습니다.`); + }); + + // 연결 해제 (Disconnect) + socket.on("disconnect", () => { + console.log("소켓 연결 해제:", socket.id); + }); +}; + +// == 메인 초기화 함수 == +export const initSocket = (httpServer: HttpServer) => { + const io = new SocketIOServer(httpServer, { + cors: { + origin: [ + "http://localhost:3000", + "http://localhost:5173", + "https://www.promptplace.kr", + "https://promptplace-develop.vercel.app", + ], + credentials: true + } + }); + + // 인증 미들웨어 + io.use(async (socket, next) => { + + try { + const token = socket.handshake.auth?.token as string | undefined; + if (!token) { + return next(new AppError("토큰이 없거나 형식이 잘못되었습니다.", 401, "Unauthorized")); + } + const decoded = jwt.verify(token, process.env.JWT_SECRET!) as { id: number}; + + const user = await prisma.user.findUnique({ + where: { + user_id: decoded.id + }, + }); + + if (!user) { + return next(new AppError("존재하지 않는 사용자입니다.", 401, "Unauthorized")); + } + + socket.data.user = user; + socket.data.userId = user.user_id; + + next(); + + } catch (err) { + const error = err as Error + if(error.name === "TokenExpiredError") { + return next(new AppError("토큰이 만료되었습니다.", 401, "Unauthorized")); + } + else if (error.name === "JsonWebTokenError") { + return next(new AppError("유효하지 않은 토큰입니다.", 401, "Unauthorized")); + } + console.error("Socket 인증 오류:", err); + next(new AppError("알 수 없는 오류가 발생했습니다.", 500, "InternalServerError")); + } + }); + + // 연결 시작 + io.on("connection", (socket: Socket) => { + console.log("새로운 소켓 연결:", socket.id); + setupSocketEvents(io, socket); + }); +}; \ No newline at end of file diff --git a/src/socket/test-client.ts b/src/socket/test-client.ts new file mode 100644 index 0000000..7020855 --- /dev/null +++ b/src/socket/test-client.ts @@ -0,0 +1,29 @@ +import { io } from "socket.io-client"; // 클라이언트 +console.log("🔥 test-client started"); +const socket = io("http://localhost:3000", { // 서버 연결 시도 + auth: { token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MzYsImlhdCI6MTc2OTMzNjAxNiwiZXhwIjoxNzY5NDIyNDE2fQ.sAqJi77BNE2gXoH4F_gtqojxhaV8Vuu6g3AmJC3ASbQ" }, +}); + +socket.on("connect", () => { // 서버가 연결을 받아줬을 때 실행 + console.log("connected:", socket.id); // 고유 소켓 id + + // 방 입장 + socket.emit("joinRoom", { room_id: 12 }, (res: any) => { + if(!res.ok) return; + + // 메세지 전송 + socket.emit("sendMessage", { + room_id: 12, + content: "안녕하세요!", + files: [] + }); + }); + + // 메세지 수신 + socket.on("receiveMessage", (data) => { + console.log(`[on]- receiveMessage 성공: 유저 ${data.sender_id}님이 "${data.content}"를 보냈습니다.`); + }); +}); + +// 서버 커스텀 에러 이벤트 +socket.on("error", (e) => console.log("server error:", e)); diff --git a/src/utils/map.ts b/src/utils/map.ts new file mode 100644 index 0000000..cb53d7b --- /dev/null +++ b/src/utils/map.ts @@ -0,0 +1,8 @@ +import { AttachmentType } from "@prisma/client"; + +export const mapMimeTypeToEnum = (mimeType: string): AttachmentType => { + if (mimeType.startsWith("image/")) { + return AttachmentType.IMAGE; + } + return AttachmentType.FILE; +}; \ No newline at end of file From 60f53576fc0d5081d25b50b2a6ac25300e0594cc Mon Sep 17 00:00:00 2001 From: Arin0303 Date: Mon, 26 Jan 2026 01:49:21 +0900 Subject: [PATCH 2/5] =?UTF-8?q?chor:=20=EB=A9=94=EC=84=B8=EC=A7=80=20?= =?UTF-8?q?=EC=88=98=EC=8B=A0=20=EC=97=90=EB=9F=AC=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/chat/dtos/chat.dto.ts | 46 ++++++++++++++++++++++++++++++- src/chat/services/chat.service.ts | 4 ++- src/socket/server.ts | 10 +++---- src/socket/test-client.ts | 2 +- 4 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/chat/dtos/chat.dto.ts b/src/chat/dtos/chat.dto.ts index 02b24ed..a9ae412 100644 --- a/src/chat/dtos/chat.dto.ts +++ b/src/chat/dtos/chat.dto.ts @@ -214,4 +214,48 @@ export class ChatRoomListResponseDto { // == 채팅방 고정 토글 export interface TogglePinResponseDto { is_pinned: boolean; -} \ No newline at end of file +} + +// == 소켓 메세지 수신 +export class SocketReceivedMessageDTO { + message_id!: number; + + sender!: { + user_id: number; + nickname: string; + profile_img: string | null; + }; + + content!: string; + sent_at!: string; + + attachments!: { + type: string; + url: string; + name: string; + size: number; + }[]; + + static from(prismaMessage: any): SocketReceivedMessageDTO { + const dto = new SocketReceivedMessageDTO(); + + dto.message_id = prismaMessage.message_id; + dto.content = prismaMessage.content; + dto.sent_at = prismaMessage.sent_at.toISOString(); + + dto.sender = { + user_id: prismaMessage.user.user_id, + nickname: prismaMessage.user.nickname, + profile_img: prismaMessage.user.profileImage?.url ?? null, + }; + + dto.attachments = prismaMessage.attachments.map((a: any) => ({ + type: a.type, + url: a.url, + name: a.name, + size: a.size, + })); + + return dto; + } +} diff --git a/src/chat/services/chat.service.ts b/src/chat/services/chat.service.ts index e27b19c..9e313a1 100644 --- a/src/chat/services/chat.service.ts +++ b/src/chat/services/chat.service.ts @@ -6,6 +6,7 @@ import { ChatRoomListResponseDto, ChatFilterType, TogglePinResponseDto, + SocketReceivedMessageDTO } from "../dtos/chat.dto"; import { getPresignedUrl } from "../../middlewares/s3.util"; import { mapMimeTypeToEnum } from "../../utils/map"; @@ -176,7 +177,8 @@ export class ChatService { contentType: mapMimeTypeToEnum(f.contentType) })) - return this.chatRepo.saveMessage(roomId, senderId, content, formattedFiles); + const savedMessage = await this.chatRepo.saveMessage(roomId, senderId, content, formattedFiles); + return SocketReceivedMessageDTO.from(savedMessage); } } diff --git a/src/socket/server.ts b/src/socket/server.ts index 38e7a13..fd43949 100644 --- a/src/socket/server.ts +++ b/src/socket/server.ts @@ -48,10 +48,10 @@ const setupSocketEvents = (io: SocketIOServer, socket: Socket) => { files: files || [] }); console.log(`[on]- sendMessage 성공: 유저 ${userId}님이 "${content}"를 보냈습니다.`); - // 같은 방에 있는 전체 참여자가 메세지 수신 - io.to(room_id.toString()).emit("receiveMessage", savedMessage); - ack?.({ok: true, message: savedMessage}); + + // 메세지 수신(broadcast) + io.to(room_id.toString()).emit("receiveMessage", savedMessage); } catch (err: any) { console.error("Error:", err); @@ -60,8 +60,8 @@ const setupSocketEvents = (io: SocketIOServer, socket: Socket) => { }); // 방 나가기 (뒤로가기) - socket.on("leaveRoom", (roomId: string) => { - socket.leave(roomId); + socket.on("leaveRoom", (roomId: Number) => { + socket.leave(String(roomId)); console.log(`유저 ${userId}님이 방 ${roomId}을 나갔습니다.`); }); diff --git a/src/socket/test-client.ts b/src/socket/test-client.ts index 7020855..df0f7c8 100644 --- a/src/socket/test-client.ts +++ b/src/socket/test-client.ts @@ -21,7 +21,7 @@ socket.on("connect", () => { // 서버가 연결을 받아줬을 때 실행 // 메세지 수신 socket.on("receiveMessage", (data) => { - console.log(`[on]- receiveMessage 성공: 유저 ${data.sender_id}님이 "${data.content}"를 보냈습니다.`); + console.log(`[on]- receiveMessage 성공: 유저 ${data.sender.user_id}님이 "${data.content}"를 보냈습니다.`); }); }); From 4aa5023c85376aeb079277aa3068db919eac6182 Mon Sep 17 00:00:00 2001 From: Arin0303 Date: Mon, 26 Jan 2026 01:54:41 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20=ED=86=A0=ED=81=B0=20=EA=B0=9C?= =?UTF-8?q?=EC=9D=B8=EC=A0=95=EB=B3=B4=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/socket/test-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/socket/test-client.ts b/src/socket/test-client.ts index df0f7c8..8f677a8 100644 --- a/src/socket/test-client.ts +++ b/src/socket/test-client.ts @@ -1,7 +1,7 @@ import { io } from "socket.io-client"; // 클라이언트 console.log("🔥 test-client started"); const socket = io("http://localhost:3000", { // 서버 연결 시도 - auth: { token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MzYsImlhdCI6MTc2OTMzNjAxNiwiZXhwIjoxNzY5NDIyNDE2fQ.sAqJi77BNE2gXoH4F_gtqojxhaV8Vuu6g3AmJC3ASbQ" }, + auth: { token: "" }, }); socket.on("connect", () => { // 서버가 연결을 받아줬을 때 실행 From e313d0fd1c48160529fd8ad33d89044ba2368c5d Mon Sep 17 00:00:00 2001 From: Arin0303 Date: Tue, 27 Jan 2026 02:25:49 +0900 Subject: [PATCH 4/5] =?UTF-8?q?feat:=20=EC=B1=84=ED=8C=85=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EA=B8=B0=EB=8A=A5=20=EC=84=9C=EB=B2=84,=20?= =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84,=201=EC=B0=A8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/socket/server.ts | 21 +++++++++++---------- src/socket/test-client.ts | 38 +++++++++++++++++++++++++++----------- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/socket/server.ts b/src/socket/server.ts index fd43949..cba1457 100644 --- a/src/socket/server.ts +++ b/src/socket/server.ts @@ -22,7 +22,7 @@ const setupSocketEvents = (io: SocketIOServer, socket: Socket) => { socket.join(String(roomId)); - ack(({ok: true, room_id: roomId})); + ack(({ok: true, room_id: roomId, message: "사용자가 방을 입장했습니다."})); console.log(`[on]-joinRoom 성공: 유저 ${userId}님이 ${roomId}번 방에 입장했습니다.`); @@ -48,26 +48,27 @@ const setupSocketEvents = (io: SocketIOServer, socket: Socket) => { files: files || [] }); console.log(`[on]- sendMessage 성공: 유저 ${userId}님이 "${content}"를 보냈습니다.`); - ack?.({ok: true, message: savedMessage}); + ack?.({ok: true, message: "메세지가 성공적으로 전송되었습니다."}); // 메세지 수신(broadcast) io.to(room_id.toString()).emit("receiveMessage", savedMessage); } catch (err: any) { console.error("Error:", err); - ack?.({ok: false, message: err.message}); + ack?.({ok: false, message: "메세지 전송/수신 중 오류가 발생했습니다."}); } }); // 방 나가기 (뒤로가기) - socket.on("leaveRoom", (roomId: Number) => { - socket.leave(String(roomId)); - console.log(`유저 ${userId}님이 방 ${roomId}을 나갔습니다.`); - }); + socket.on("leaveRoom", (roomId: number, ack: any) => { + try { + socket.leave(String(roomId)); + console.log(`유저 ${userId}님이 방 ${roomId}을 나갔습니다.`); + ack?.({ok: true, message: "방을 성공적으로 나갔습니다."}); + } catch(err: any) { + ack?.({ok: false, message: "방 나가기 중 오류가 발생했습니다."}); - // 연결 해제 (Disconnect) - socket.on("disconnect", () => { - console.log("소켓 연결 해제:", socket.id); + } }); }; diff --git a/src/socket/test-client.ts b/src/socket/test-client.ts index 8f677a8..3e2afa8 100644 --- a/src/socket/test-client.ts +++ b/src/socket/test-client.ts @@ -1,29 +1,45 @@ import { io } from "socket.io-client"; // 클라이언트 console.log("🔥 test-client started"); const socket = io("http://localhost:3000", { // 서버 연결 시도 - auth: { token: "" }, + auth: { token: "ds.eyJpZCI6MzYsImlhdCI6MTc2OTQ0NjMxMiwiZXhwIjoxNzY5NTMyNzEyfQ.Z1yxVhEaCw5uNT0W8T7neAJ4QRt4FX4MdgYQhmc2XnQ" }, }); socket.on("connect", () => { // 서버가 연결을 받아줬을 때 실행 - console.log("connected:", socket.id); // 고유 소켓 id + console.log("[on]-connected:", socket.id); // 고유 소켓 id // 방 입장 socket.emit("joinRoom", { room_id: 12 }, (res: any) => { - if(!res.ok) return; - + if(!res.ok) { + console.log("[emit]-joinRoom"); + return; + } // 메세지 전송 socket.emit("sendMessage", { room_id: 12, content: "안녕하세요!", files: [] - }); - }); + }, (ack:any) => { + if(!ack.ok) { + console.log("[emit]-sendMessage"); + return; + } + }); - // 메세지 수신 - socket.on("receiveMessage", (data) => { - console.log(`[on]- receiveMessage 성공: 유저 ${data.sender.user_id}님이 "${data.content}"를 보냈습니다.`); - }); + // 메세지 수신 + socket.on("receiveMessage", (data) => { + console.log("[on]-receiveMessage") + console.log(`[on]- receiveMessage 성공: 유저 ${data.sender.user_id}님이 "${data.content}"를 보냈습니다.`); + + // 방 나가기 + socket.emit("leaveRoom", { room_id: 12}, (ack: any) => { + console.log("[emit]-leaveRoom") + if(!ack.ok) return; + }); + }); + }); }); // 서버 커스텀 에러 이벤트 -socket.on("error", (e) => console.log("server error:", e)); +socket.on("connect_error", (err) => { + console.log("[on]-connect_error", err); +}); From 3be55fbf6f479026d77b71e4a9fec603fc120b49 Mon Sep 17 00:00:00 2001 From: Arin0303 Date: Thu, 29 Jan 2026 13:00:48 +0900 Subject: [PATCH 5/5] =?UTF-8?q?chore:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/socket/test-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/socket/test-client.ts b/src/socket/test-client.ts index 3e2afa8..49d7941 100644 --- a/src/socket/test-client.ts +++ b/src/socket/test-client.ts @@ -1,7 +1,7 @@ import { io } from "socket.io-client"; // 클라이언트 console.log("🔥 test-client started"); const socket = io("http://localhost:3000", { // 서버 연결 시도 - auth: { token: "ds.eyJpZCI6MzYsImlhdCI6MTc2OTQ0NjMxMiwiZXhwIjoxNzY5NTMyNzEyfQ.Z1yxVhEaCw5uNT0W8T7neAJ4QRt4FX4MdgYQhmc2XnQ" }, + auth: { token: "" }, }); socket.on("connect", () => { // 서버가 연결을 받아줬을 때 실행