From c39fb8e16bc4636d6acba3aac72a540ce3705c89 Mon Sep 17 00:00:00 2001 From: Sue Date: Mon, 16 Jun 2025 17:59:31 +0900 Subject: [PATCH 1/3] =?UTF-8?q?=EC=8A=A4=ED=94=84=EB=A6=B0=ED=8A=B8=20?= =?UTF-8?q?=EC=A0=9C=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 50 + Dockerfile | 15 + constant/ExceptionMessage.ts | 15 + docker-compose.yml | 42 + docker-entrypoint.sh | 7 + google-auth-setting.png | Bin 0 -> 201442 bytes openapi.yaml | 730 +++++ package-lock.json | 2538 +++++++++++++++++ package.json | 50 + .../20250613074707_new_table/migration.sql | 107 + prisma/migrations/migration_lock.toml | 3 + prisma/mocks/articleMocks.js | 32 + prisma/mocks/comments.js | 62 + prisma/mocks/likeMocks.js | 14 + prisma/mocks/productMocks.js | 42 + prisma/mocks/userMocks.js | 10 + prisma/prismaClient.ts | 2 + prisma/schema.prisma | 90 + prisma/seed.js | 37 + .../article/CreateArticleCommentHandler.ts | 64 + .../article/CreateArticleHandler.ts | 54 + .../article/CreateArticleLikeHandler.ts | 70 + .../article/DeleteArticleHandler.ts | 36 + .../article/DeleteArticleLikeHandler.ts | 72 + .../article/GetArticleCommentListHandler.ts | 78 + src/application/article/GetArticleHandler.ts | 59 + .../article/GetArticleListHandler.ts | 94 + .../article/UpdateArticleHandler.ts | 77 + src/application/auth/AuthByGoogleHandler.ts | 88 + src/application/auth/RefreshTokenHandler.ts | 46 + .../auth/SignInLocalUserHandler.ts | 53 + .../auth/SignUpLocalUserHandler.ts | 70 + .../comment/DeleteCommentHandler.ts | 39 + .../comment/UpdateCommentHandler.ts | 69 + .../product/CreateProductCommentHandler.ts | 61 + .../product/CreateProductHandler.ts | 40 + .../product/CreateProductLikeHandler.ts | 61 + .../product/DeleteProductHandler.ts | 37 + .../product/DeleteProductLikeHandler.ts | 63 + .../product/GetProductCommentListHandler.ts | 79 + src/application/product/GetProductHandler.ts | 49 + .../product/GetProductListHandler.ts | 85 + .../product/UpdateProductHandler.ts | 63 + .../user/GetUserFavoriteListHandler.ts | 78 + .../user/GetUserProductListHandler.ts | 65 + src/application/user/GetUserProfileHandler.ts | 34 + .../user/UpdateUserPasswordHandler.ts | 70 + .../user/UpdateUserProfileHandler.ts | 45 + src/constant/ExceptionMessage.js | 22 + src/domain/Article.js | 71 + src/domain/Comment.js | 60 + src/domain/Like.js | 44 + src/domain/Product.js | 90 + src/domain/User.js | 68 + src/exceptions/BadRequestException.js | 11 + src/exceptions/ForbiddenException.js | 11 + src/exceptions/HttpException.js | 19 + .../InternalServerErrorException.js | 11 + src/exceptions/NotFoundException.js | 11 + .../UnprocessableEntityException.js | 11 + src/infra/AuthTokenManager.ts | 92 + src/infra/GoogleOAuthAdapter.ts | 60 + src/infra/UserPasswordBuilder.ts | 10 + src/infra/prismaClient.ts | 3 + src/interface/ArticleRouter.ts | 216 ++ src/interface/AuthRouter.ts | 112 + src/interface/CommentRouter.ts | 50 + src/interface/ImageRouter.ts | 48 + src/interface/ProductRouter.ts | 217 ++ src/interface/UserRouter.ts | 113 + src/interface/readme.md | 52 + .../article/CreateArticleRequestStruct.ts | 20 + .../article/GetArticleListRequestStruct.ts | 40 + .../article/UpdateArticleRequestStruct.ts | 5 + .../structs/auth/RefreshTokenRequestStruct.ts | 5 + .../structs/auth/SignInRequestStruct.ts | 7 + .../structs/auth/SignUpRequestStruct.ts | 9 + .../comment/CreateCommentRequestStruct.ts | 5 + .../comment/GetCommentListRequestStruct.ts | 12 + .../comment/UpdateCommentRequestStruct.ts | 5 + .../product/CreateProductRequestStruct.ts | 9 + .../product/GetProductListRequestStruct.ts | 27 + .../product/UpdateProductRequestStruct.ts | 5 + .../GetMyFavoritesProductListRequestStruct.ts | 23 + .../user/GetMyProductListRequestStruct.ts | 24 + .../user/UpdatePasswordRequestStruct.ts | 7 + .../user/UpdateProfileRequestStruct.ts | 5 + src/interface/utils/AuthN.ts | 31 + src/interface/utils/asyncErrorHandler.ts | 50 + src/main.ts | 59 + src/readme.md | 73 + src/types/article.ts | 17 + src/types/error.ts | 48 + src/types/express.d.ts | 9 + src/types/product.ts | 15 + swagger/components.js | 0 swagger/index.js | 0 swagger/info.js | 0 swagger/paths.js | 0 swagger/swagger.js | 21 + tsconfig.json | 117 + 101 files changed, 7695 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 constant/ExceptionMessage.ts create mode 100644 docker-compose.yml create mode 100644 docker-entrypoint.sh create mode 100644 google-auth-setting.png create mode 100644 openapi.yaml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 prisma/migrations/20250613074707_new_table/migration.sql create mode 100644 prisma/migrations/migration_lock.toml create mode 100644 prisma/mocks/articleMocks.js create mode 100644 prisma/mocks/comments.js create mode 100644 prisma/mocks/likeMocks.js create mode 100644 prisma/mocks/productMocks.js create mode 100644 prisma/mocks/userMocks.js create mode 100644 prisma/prismaClient.ts create mode 100644 prisma/schema.prisma create mode 100644 prisma/seed.js create mode 100644 src/application/article/CreateArticleCommentHandler.ts create mode 100644 src/application/article/CreateArticleHandler.ts create mode 100644 src/application/article/CreateArticleLikeHandler.ts create mode 100644 src/application/article/DeleteArticleHandler.ts create mode 100644 src/application/article/DeleteArticleLikeHandler.ts create mode 100644 src/application/article/GetArticleCommentListHandler.ts create mode 100644 src/application/article/GetArticleHandler.ts create mode 100644 src/application/article/GetArticleListHandler.ts create mode 100644 src/application/article/UpdateArticleHandler.ts create mode 100644 src/application/auth/AuthByGoogleHandler.ts create mode 100644 src/application/auth/RefreshTokenHandler.ts create mode 100644 src/application/auth/SignInLocalUserHandler.ts create mode 100644 src/application/auth/SignUpLocalUserHandler.ts create mode 100644 src/application/comment/DeleteCommentHandler.ts create mode 100644 src/application/comment/UpdateCommentHandler.ts create mode 100644 src/application/product/CreateProductCommentHandler.ts create mode 100644 src/application/product/CreateProductHandler.ts create mode 100644 src/application/product/CreateProductLikeHandler.ts create mode 100644 src/application/product/DeleteProductHandler.ts create mode 100644 src/application/product/DeleteProductLikeHandler.ts create mode 100644 src/application/product/GetProductCommentListHandler.ts create mode 100644 src/application/product/GetProductHandler.ts create mode 100644 src/application/product/GetProductListHandler.ts create mode 100644 src/application/product/UpdateProductHandler.ts create mode 100644 src/application/user/GetUserFavoriteListHandler.ts create mode 100644 src/application/user/GetUserProductListHandler.ts create mode 100644 src/application/user/GetUserProfileHandler.ts create mode 100644 src/application/user/UpdateUserPasswordHandler.ts create mode 100644 src/application/user/UpdateUserProfileHandler.ts create mode 100644 src/constant/ExceptionMessage.js create mode 100644 src/domain/Article.js create mode 100644 src/domain/Comment.js create mode 100644 src/domain/Like.js create mode 100644 src/domain/Product.js create mode 100644 src/domain/User.js create mode 100644 src/exceptions/BadRequestException.js create mode 100644 src/exceptions/ForbiddenException.js create mode 100644 src/exceptions/HttpException.js create mode 100644 src/exceptions/InternalServerErrorException.js create mode 100644 src/exceptions/NotFoundException.js create mode 100644 src/exceptions/UnprocessableEntityException.js create mode 100644 src/infra/AuthTokenManager.ts create mode 100644 src/infra/GoogleOAuthAdapter.ts create mode 100644 src/infra/UserPasswordBuilder.ts create mode 100644 src/infra/prismaClient.ts create mode 100644 src/interface/ArticleRouter.ts create mode 100644 src/interface/AuthRouter.ts create mode 100644 src/interface/CommentRouter.ts create mode 100644 src/interface/ImageRouter.ts create mode 100644 src/interface/ProductRouter.ts create mode 100644 src/interface/UserRouter.ts create mode 100644 src/interface/readme.md create mode 100644 src/interface/structs/article/CreateArticleRequestStruct.ts create mode 100644 src/interface/structs/article/GetArticleListRequestStruct.ts create mode 100644 src/interface/structs/article/UpdateArticleRequestStruct.ts create mode 100644 src/interface/structs/auth/RefreshTokenRequestStruct.ts create mode 100644 src/interface/structs/auth/SignInRequestStruct.ts create mode 100644 src/interface/structs/auth/SignUpRequestStruct.ts create mode 100644 src/interface/structs/comment/CreateCommentRequestStruct.ts create mode 100644 src/interface/structs/comment/GetCommentListRequestStruct.ts create mode 100644 src/interface/structs/comment/UpdateCommentRequestStruct.ts create mode 100644 src/interface/structs/product/CreateProductRequestStruct.ts create mode 100644 src/interface/structs/product/GetProductListRequestStruct.ts create mode 100644 src/interface/structs/product/UpdateProductRequestStruct.ts create mode 100644 src/interface/structs/user/GetMyFavoritesProductListRequestStruct.ts create mode 100644 src/interface/structs/user/GetMyProductListRequestStruct.ts create mode 100644 src/interface/structs/user/UpdatePasswordRequestStruct.ts create mode 100644 src/interface/structs/user/UpdateProfileRequestStruct.ts create mode 100644 src/interface/utils/AuthN.ts create mode 100644 src/interface/utils/asyncErrorHandler.ts create mode 100644 src/main.ts create mode 100644 src/readme.md create mode 100644 src/types/article.ts create mode 100644 src/types/error.ts create mode 100644 src/types/express.d.ts create mode 100644 src/types/product.ts create mode 100644 swagger/components.js create mode 100644 swagger/index.js create mode 100644 swagger/info.js create mode 100644 swagger/paths.js create mode 100644 swagger/swagger.js create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..1f2ad60f --- /dev/null +++ b/.gitignore @@ -0,0 +1,50 @@ +# ------------------------ +# ✅ 공통 +# ------------------------ +.DS_Store +*.pem +.vscode +.github + +# 환경 변수 +.env* +*.env + +# 로그 파일 +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# 커버리지 +/coverage + +# 빌드 +/build +/dist + +# ------------------------ +# ✅ Node.js dependencies +# ------------------------ +/node_modules + +# ------------------------ +# ✅ yarn / pnpm / berry +# ------------------------ +.pnp.* +/.pnp +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# ------------------------ +# ✅ BE 관련 +prisma/dev.db +prisma/dev.db-journal +.env +node_modules +dist/* +!BE/dist/index.js +/src/generated/prisma diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..57bbc85b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM node:20.15.1-alpine3.19 + +ENV DOCKERIZE_VERSION v0.7.0 +RUN apk update --no-cache \ + && apk add --no-cache wget openssl \ + && wget -O - https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz | tar xzf - -C /usr/local/bin \ + && apk del wget + +WORKDIR /app + +EXPOSE 3000 + +COPY ./docker-entrypoint.sh ./docker-entrypoint.sh +RUN ["chmod", "+x", "./docker-entrypoint.sh"] +ENTRYPOINT ["sh", "./docker-entrypoint.sh"] diff --git a/constant/ExceptionMessage.ts b/constant/ExceptionMessage.ts new file mode 100644 index 00000000..7664d74c --- /dev/null +++ b/constant/ExceptionMessage.ts @@ -0,0 +1,15 @@ +/** + * [에러 메시지 상수] + * + * 에러 메시지가 반복적으로 사용되는 경우, 상수로 관리하는 것이 효율적입니다. + * + * 여러 장점들: + * - 오타 방지 + * - 추후 에러메시지 변경에 유리 + * - ... + */ +export const ExceptionMessage = { + ARTICLE_NOT_FOUND: 'Article not found', + PRODUCT_NOT_FOUND: 'Product not found', + COMMENT_NOT_FOUND: 'Comment not found', +}; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..bee056aa --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,42 @@ +version: '2.2' + +services: + pg-db: + image: postgres:16.3-alpine3.19 + attach: false + container_name: panda-market-db + restart: always + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=panda-market + networks: + - panda-market-network + ports: + - '15432:5432' + command: [postgres] + + service: + container_name: panda-market-app + build: + context: . + dockerfile: ./Dockerfile + restart: no + environment: + # Application + - NODE_ENV=development + - DATABASE_URL=postgresql://postgres:postgres@pg-db:5432/panda-market + - JWT_ACCESS_TOKEN_SECRET=$3cr3t + - JWT_REFRESH_TOKEN_SECRET=$3cr3t + networks: + - panda-market-network + ports: + - '13000:3000' + volumes: + - .:/app/ + depends_on: + - pg-db + +networks: + panda-market-network: + name: panda-market-network diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 00000000..05d8ca5b --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,7 @@ +dockerize -wait tcp://pg-db:5432 -timeout 60s + +npx prisma generate +npx prisma migrate deploy +npm run seed + +npm start \ No newline at end of file diff --git a/google-auth-setting.png b/google-auth-setting.png new file mode 100644 index 0000000000000000000000000000000000000000..9d2a2e2b505c1060bdd6fd8e2655fa4708faadd9 GIT binary patch literal 201442 zcmb5W1za4>(l?3*2o3=PB)9|*5Zr@%a98H{^QACmylf3mdAX-5S zkWFKH{TL0Y;$>IAR=Gr1gw{}6bOh$9%+nGQb?aK$ex!H{0tvCCr08{88+r$p}w?eCfvjCWZ1uEg&wxv-#QN} zq^cd7f5dT({K18jewP6BiEm(#><#{Q-Sul|ORZPkHFAQ>HACp_Xp`+`O&f}({jV(y zH6YPaIO8C_xvUDW`u=nA;V+ElHEoG9IYYMAQPvlq3S|dwSwvIV1Dup(&R*G^vvS60I=rWs7M^sCQ=dv&1G$_HKB2 zjee6ESPdn@p9OTE5g3SaLG?DtY{CYol;X);tsOt^<7my^)BeKgmLgq7#aa%0Lh#5_ z66b9oM=mPR(i4g0g#EdfUF;CbH(o;8bW8q|VQgA%0y;v6z*bixK!fEfl4vR>Q08|1 z308K*f(lhO;fo@7o@jz!rGG=DS{g1U_9Nt=bk6$)JO^`tj`tk*5S?0rS#iwz6$ zS=Kvx@i{v75&?R4rs;!S}JB&0f8Z5$A+V_ZqGF9liHN_Q!2U3wRnp6+k8tNKOBO00&bo#0(`hUV27G zQ1Ttq0@E3B@5e|P`Y+hr!D&jys_cwYL{&s!f~5on29rd3mgH~D6?C5xV-qTqycNOy zN|stR%tATrjLr|rIUW7dXDLs>YJ*!YsXj~|o=bM-}VDO`Qy~&krV_;)< z12)i>!u*5s2k!=1|JFe24{&k^heWyLo0r;0+GcF!Z(iuV)7;j5s*}%Z$nuEI`i0XQ z(Q;4e_iwO@EQ{AGzUd6;%~iP>l*C1T+0_s$dA4h~i@d92CxNTX`=$C_NgdsshM|a~ zjDg~;u8@+DQM2)NmTTN&rSES-3lb}96gNVYQU_8EGHTOmJ!_{%OS9cUQ}UCurLa=b z$=8#@lWLP(c5Zym^{?tV>~QVWC&zXRcR%kM>_R51O7TYvhdpwJ(@g~7-}dDxzJ`^^ zrS$cH`<#1^jFj?{V_|zZlsFhM+A;ca&2oHl>d`E{PQA)8Y%zzKW~xt_Y?&Y`u(a+% zk3yEhjl#xZGhQFw_F2x_SfMnbSRfjZb;~U?X*hi|eUopqjg*x%h}52)f*mkA38oYP z4K7SIFL4~O|Kh;c7jMw4IcYSsm~=Rwnw!Y5Fmdu*3VXX|Q9E8Zwi#2A&N1rIcz`NW zY7!3>hLA#>FYqqt;D&Ho48L%+@WD z9UT5pc2&}FoneY&(2*%DfHCkH0o5DQBhjC)-aMIkhK6yC;fU+avt-t8<}dvc(;D3w z*Pr6;lbToSDlhn7G1Q5k$)@S(1N7|$`F!7BtsLPKQW0j-QOQ%tE72Q$SteTCYZ)*dT%=g8rHM2;K-=r3i=;^l_`3Z4;NFnSq2EmkGk) ztV*TyUYSLaeADM-@XA^9PV+L~UjlDjSd*G^+699nlM{aWuV@cO3nIaoShJ zq?kS_!W__cH<>3i!Y{(UVSkSyqM$3D%kLeh&TmVvOy5Wc*L0lwfV@+>&n7!QI98dFSMrl>J$$ z1zPBs7KQ#?lUBgiX13{|=78{^*uWm^E7mDJ--Yi`cTgi7HYb+h6M5k~JGcZl10S~E zDD?~^-p$;l!%d%k6#jX`ySBCaVHYC`&tHT=a6*ej*S%owbIZ-w-W63omzX;1ZH3ze z5&5=Q{;7;h1kA5^tUd1& z8Ge2$JhJ2t@!83lZnM8MxO8?OIo#il-1q+Zd7-bFMyRNV7Yu_@i(sQ0aJ=%(c3;I?06F*$kOFW=&$o2NXdug@NgNQ{|3yU;BHiqq~03-m9i~j<@Pc$WM5d^BIs9sb0TwXGfY4 z`RK3zvY>|l@F4gEm8;`{ca&;palj2dGvZ}90pAYiew0VpreDO zg(0u{E*z_euEn2g&*-fj-IV};!QF%ArG(APgh|p{;61UHJq649EMXGDzD=>xSF~1D zMPfyyF_F-biI7kcDP+VK37G<61piJWAt@nK{wuAC{Nk@XC`d@*wn%7yRHK){4{9(dBnJNTQy?h@_*H zn>mfAqk|Jr*i($|j~v2?^lvg39nBwE-0a2Z^i|)`NISb)(eQJ=?T>!_S)J(b!i05zR<6>{j*eDNZsPw)T=b8c{>Q_A+W9Br z8(U8+2Yne^1Sb&DCvl#a+&q7={pX|qmDS)MR&E}_m;cWCZ;$?s^>-(PHLZZo4(`8& zsN-boCXT4_edMohS$O9#d!A%vJA`;htc1Y^FHV%PsFpe>`F&uZdLmcMwn zl|H=h$e(HX3zoRROW^^!xlPP;xJbXu&MFIE)YaG5r@=%=5B>7M_JV+O{tazmV>PY5CAz{^y=dl_{ZZHS7YGAHMCeFyZCU?dL0QdI^7FAye^Ky$O z{&$7NOYYFdCu!vuq~vG-^T#mP4aiXvy<>%2CeJ&!3SBn_#&+Kd|5wfWv-QaoCg4R3IHwC=D)9xa1dadeRv;5$D$|UnqhN!K+~iTtE>LXn za+3Z;qNFvke#5PE`t|%_&AVfw*{|=Do;*2<;;dcLxX3|MpfSN*-`pGkOqMAo^~U5o zW}+5I-T5iy+h=Vlqlr*5{v(M@$(9AOx#z1`|DY;AEs(Lv-t=Uk5NbYvS7BmmBnM-G zJt|}VlPhR~*wcAHp2x3uXhPr{>F?ppeL~cUQs}-XuuZl(hj%bH5Fr-Jrc0Ey6-6RI zN&in}>L@!drrWdJa{tUH$w*B7V3B0^j|YY>dJpgF#bh~Z4F8hQ3uF|FKJ{dY2g70` z@!Ox$`R-+;nnfOtELw#sSZ86Xze|z4Ae^bSi7N-x%30rW>$PWJd;l!hz5it`RAvf( zOI{AlIsDn4UH7gXJW;NrHSl84=h5Ht51PReJ=hNhlB%Nh`aW}k|F zzhJ*Qa>4x9#4?c1HJ%l<9dG&!Ah_PWn%43+P2*b@Bp^~iMJeO2NR&t!PPt|y`;o|} zZ27(SQ_Vg2yv7PHQ3JeGgZ8+3ZR9Im4`}AO;?6?6#cZ`939qUe!F6^nffE(F0=RNr zoWHf3nR(enTLkkd3!RJ8Jr1!z+T6>YK7ph3g zfK2og&6@LGr#<7eUM2J41G{0ZAnT{+5D!d%ePel&7u#mk5GU(+zK4ZS-8HSN_8@(M z2~#5fXVe1G#^aFKq4N#473d(~VTym7Wt>8;8_`UQMVZQt zZH@0si%6~nC>JBOJuOweU&G5zQd90t}Qrw=gO|za;6AfzS(iSZU4EL*GlDMwdXWD>6!obKL)# zi%3`uL6QfC^==%SiyT-wHIES}M8lMPM-4w0Oh1CkI2Pnc>S#p0_6%6fD_XxE?NwNM zI7_IF75&idqir{YP>%7oXzX3jBU=h;=kKO9SYw0!T5EXHC8eZ-VE*JAy?@biJ^yOS zJLrT7lYjupWUbo2qo0A`MW)Rilfb2uVsWAlqEFuxmt$<5?T&p;JB;Y2ko1mb;@S+L zeg#r7k2E2693_U;rR!T8d#)lLhw;Pn{DT?8_ExhuRwrAf9cS%_E~paLY^>9lyiOV{G5q!%(87sg{a6|3=pmA+?G0k&zHCUJHJno5rEKQIKBf+}n z;`q#SC7;fd=kVsg2l)SLl30=k_5WJ>$l$NVzHP{+>B{uJ6oamv-_m zLYkbIpg)_Z2fhXTOC|n0s{bQBp3%9DPn~+a)J`?thfoabts{5mIlDqaCEP6`fdLm9 zHoN>^ZEmr_9WO*chjs8^bfn8xUxnD^V*H|sdtyf-6%m@?^dGy=I_Ge5v2$``Ux zT4%?_em&~D|96e>zjRSjnD8YjY-fV`g3$-l3=uS>yiPec`azFMl{|%OR4^<#Ms|kf z2Yht)+`{a)%@w3V7hm-09K|Lz>e&D#36mw(##%$l?k)3YOPgWVT0aHfn;UV0wDkl}Z-M4M~tkxn7e0g`W zFZWl5)9v5Co9#CI`16Y_F#BRDsU|ZNH;|bj{xjN1T?!uXBCxRX(uE60X4?t?yn z|Nk+z`LsdJz^x+ls+j^6rXz%Ed3_-pK6s&V=cKG$4gK2t8tLr#gj?G3`lds*j!L=g zQAIvNM;$ad2zkM?LFm9h!s)hNtMQ@tiL9uER4Ag~L0oU6>4Op(<81`0&_F--(cdgi zMtIOW-_eIIe1rCWOPaw{xjHd5F`HU72~NeTpiG@C@{6m?joX~dKUMf=$@?Ezc;k(w z0*1)q-|v7pjyH-r-TO#&+x>kIqTYOL2b}E}c<6DHM(=z^#-Dkg?vL_WETJRd zR%|&`nCBdrv2#cZS4Ua-ZOm<&(bw!{h3;xLr%Z>z<~GHQy?T=*F_GzO)URBP6fz=S zuEIa2au8Ali+lq;J`9WRShH9XW8uBO|30$t)f<(3?3@Xn!TdkE^e@9ZUJ;F^PTEoc z1p+x_qUtNsSNIcV&9KI~Z9&<3Hy2-wNuF>0wFaX?L=8Qs@1hDC?S~ zrSEmD*RLR?@+Kp^d5PyOiTr*poC+^?)T{>JweEeoUiY=!QcW%OJFLv^`y@|NvB1g}5~WHn;@v1PhQJ|~V4`Qz=HBT= zi%$aSMwrBvLCb9E+mSZlMyL(=jPsj5(Z>XYYr|Im+{owY*z?NuYKcdc^=E+m_C9^F z+SErVW8W^?<@F|>8VWQ_gSPFHc`G-dS@9lwLQ~?0uvY*Zsvs8ulmxlpOte;|%y_B@ ztY17yf#A`O`iF7R%HpA+#4_ z(HD_bFom5##Cs(3J)msNRD%R5xZ z=X0f{bKfm@4EE)4W^XVZ#c zhCr%2x1;w+c{Qst^%q`rvfj^`ORMlxDbnO&re#9kWfy+(dG%W5FE{dUIsK4BSy}jD z;q1X>?{1T&XMlP65wxi+;AGTD_M`T&;wN+tV(qWp<#(C$+Mv-Zb;PvR&cNu6z}j3r z9=I6*ObxhJr6&sSw09UpqKkOx6U7~08v zK%A{79Sj`S>lI{e*-MLQr*-UTdan(wMOQZJudbujy0T$aOhC;nwu$l2)8E@( zbER8^y@74WiR*SfyJlY0);d^mihwPcrG-gtiK6eSg=BOzRw8j0CO-^BEx&90{ z7nY?+5H2OZ!j_uJ*~=;_kNPa&$gF{YW%T>1Huf=Etw)VEqg1mk?q%8)1F}ETPjafk z2*r9#d_**4J}F+jB|euSW;jeRc|N!iOO*n9ep$6z7W>Z8vmijgzINe$oUSTAzG(zQ zFV%kJ-4h~e z1%c@?;pg`E@6Nfi|8i24c!Q$2T4cChG{Ip$Jdb)dhch>tEFgp2CM^T<)c(obch>TX zxvvPX4jPx%C-wRm9e)&KyYoCD3>5Kfij8BG%msxPE!7*3Le`Srw^`@~mCt?sHEj(D z3KA(`pxl1W!aIC@Cf>?>Z8F5+~vYDdMVaXt2Dq;{vPOnqaDnhHAZ4c}pFi+Q`H-rkyV=jG-~ z4##zD1607`V&B`2P5tWpxeRJ#rYcR!*s_Fxh0mt%1Rp-}G9%&w16YzzJMP0W_dP~L zPjTR)Im90is$zXqWp*w>i`<%Xb+*LneB?&fEv|QX$_S&7^4b#x%{$qB3S;`@9lh@H zqB{Ixx+XH3a*@?xs$&|K{5g*6=Z<3rlT!>}*^|Glef23%d?594l+m!&GNnIZK8J0wtoC-6tb_CdGr&`a~1IH-1mCd(ZlvwGKR^n-Nig{XI94 zW1?R35K_)2a9;SJjf`)V&H76xX-b&aBX^$-{AoGijRUP+oPHhJsxJ6n;c`useuhQam&;(Tm z3{lwhDw9_P@Tb=x@Ek~ulFP$A`=NHrY}6RofY(L|4PzCq+7XU+sAIw zW`d^@{LxwOM^Z9zAa?{9X;8N%fZ59O}&x$5-2IUhka?hP&MP! z`)>DwXsj(qnyy0+pk-<+@>TLPmIAXQ*aDciTPyVtoe zw{O5A_>~Ep=PEzLfV3}0h-JNv(qsF8V$p&MBi0S?Jf?g)a|pyJ7uoLUt2C!Z!^xejk(V5v8U;^ zRU~Uv-7@Lkjnuu{(rrG_UYv<6Li96eAvBQzNg=y^7USWxA|)x zQf5JC6qh7|ox<+f@KwR^DCgGAk?g9>p17xHWBd4rMOA77D*yq0z}m-poD&hf`R{Y` zRpKuz*D*bt-A;C6=3Ah9Oi+VL29x1~*;;uh9FI-H~l_KTRoW-gJ0Uf5aiuyR(sPvq`$>*DaMagWb89HZ$Jx?*j z_r}BzfA6a-hX*c=Hi26-KVg9VhVYL)MU*Y^RL&QkL`=s%7kSOTLB9biWaP275j)Oe zw%}y%zwhI_3pek8Vt8Dd({il4u=0?6g?s< zDv#G0pcB(GXv9C5SBN^;g7Ptc1_+T6`kwE}k^3t7nRMtCtQA(CL|PkT5gZW>-n#XO z8l9eI4XFDKC1scK-){80tlStw=9HE`mR{( zLc_D4%gE5^q6;APR6NZwH4Q^d@eas>8WaTasd&^x)fw)@fVy9_tZ>2~F2_3S1jRZ!B*WJS+1SZ`8H{t@XA=ANV-^k_kvj|4J z(cHfI<(Y*zEM=>h_qs^9y41dP(m(pZl*mTDFeG@{eQa;O&rYm-_b9wJu1RsVohMAi z`dNie5y$>$x?U9&9OVMmvoY8M65cx(&iF3VplE8*0iO2GbwqYu|x)P7k!8}#*LN9=y$XQgZfi1p#Tc`2a5VU})G z6rMGdb{utk4?nE&IwdX9tIBycu_+0?zlZx&<+d#cV_OZ+xsMkpGn2b+kmL-TfS|aM zThD}!I;ULwis!A)clmgZx*v90_ofU|c$>B*mVFsrQa|}!%o^qRu5ypI5D0nf3tlgO zV2M0+J~`}oQ2JGJ(=zaw_*`mWHw2}NUP~GRzLvTRARaT&! z+K+9~`$L}EkItn(8*HkE_s%P@&K~sD*khJav%=8s@>|bk0$gO0N;{YtcFokat1tQp z1)GdB`M4@3)u8tNP1M8YE;`qY)}2K7gZzEb{e9MHa>+$?AC=ze>a=E_wJN#TjRx^^ zx2%`u!L4^w^TJO1WfteRpzCF}3GronOZdfek41TEat^MxtHgHy{o_8-`47vt>%{5< z_!76p@Q<=us+W$es2F(37O^{Tr#HmAZXb^2sRw8DOg}XPH%qSr!e80W$rzS&3`cRb zD^R?8x6Db`6J|m`n97j^gF?!#%Q{-cSKQNu-Agkh{Ppe@Ib$dU5{cDCrA9;!MSYwM z5%GLu|LcvG%JaSHd3S*8F{5{ox(VN*_|-D1$L%LQMohWXRj@G1UApRQVsk<3w#=#x=txrzZ+j605}kGTs64h1+?0eFydQF;V@Df1&H-r zz(dJ;i!XQRZD+nFm_-Fq`(H`4s6j^nDWaD{5Iw2xXca!Pct^$+%(Ev}6k$%z@Y_yNXTacynJdj8ha!mfd?%o3n} ze%m)962(${p9YuqBHN2T5SftiKHT;-%!O-Abd$_mt&-X2JEsa)W=&b`F@4I{<+EQo z1=aHRk03VBY^>!9qXUg-qUvdHnL2rC8j;BC#fHFbXq#2W1h1NOaq%!))Ox>Hia|E| z)gx25`4ADuO86mPjVaW+#$}kU8FYfm6A)@ObGB1HYV~fER+C%eI>&{Zzi2tO8e|I< zB9~UNd%j6tTkI~<8olsf^a8xr?k!t!#Fu*l1+jZ;L~ZAM3W->_ z8GK{4&E?#}AWFsFGO>cUF4?WxSfbX(OcHZE ztHWV2l&W=A^dTwAoXskvTn0qR)KQO)ZuDdBoF*@|)DNB~$6{md5V&RX%?mIL%dqPo z6~UZCA5ST{>+`h0<5~%F32B~sp?}=d14P`FGGQ=&_~mygmP>{$3LnyvvK`~h0w+u} zt7yNy@~JVGChQ<#(=GP{oeV*se8f(iU9mmU4MY4moc~I2l*w!kDbK?APqt z`@ljyFl&>|BCzP$)|TogT22$0GgsvNQ?S7iu2=YQTkGFeMEXhe_9yB|z{50mF?E0I zOQ3I!EGY7z-#7Jbnf*XAi|ZaoV}FBV*Ci+lVdu3nm30iJ0{k_G(T9Us0qwrcq}$b= zjK=!)uJt2pDqw+toboH5P+p~!l4kY0D%A3HDDvr~&{kF%Ixs2Bx7HtAK#GQ&P*z1% zN@|{Mkr*6{FM5>Av6>aeu3PbYpyXx8yNjL0btWW7p69MJeFKF0kN|*Lqo5pPExE zN{V$Wd@A6zJ{InNHew);#-FiNisafL6XhZxvo{CBE@=FWhEbh}k)P<0yg8)M}bx~Q{v`Dev0x?!Z zihBM5myJ`h5kS8foHteViHi(uY^pc?MMF0IRjKNmNqc@+BGq(=w-mgcFc0|fipRSfO?3ZUZtjDPm=aCHmJT;TeogI7 z;JaopZW!AZGu5KzojCTT7WFA&pXQ3*%%I6$>N%ud+N{wMR1fsYeMF)RKDmFF5^Sut zx91s#4LUo1-0w69)0eRXnVJ()tGNlJ9Kd+|E6sPDn$z*?-Qr{&E`Oygi5|N|>bZd` zD|^U%gwcxJn|JVUtjoV0OJjCM?dZ?O%EWsMsFBM#{Z?j~ z{oIHAK2VhqIOQaC2#S{xGq;z>s_6Uo*tPmzpEMLlh_{!s$C|I;HqMg)JcF{PisB}; zNz*%62Rn?MAtv`X_MdCcs8|ogGfO#;ve#P7mT5RHbW61r4K#A9DrPAsoO45V)$1l{ z%X|CS2K(saL|=Pfh>jU(N9a8aprdk4bn}qkuEs59v1@GU+Nfm`JWy%UUZ|$YnT>2weJbT zT|epiu<^AAZ;T4B{c2U2F*#;xs!}=}@7ENJV7m2n-RO*#x|l`Of#1zp3h$_(^o34J z480B=8%t&0?np0yZlq(GghIK;=D7V zO&d@k5_T0M!K4TX#_lzcElT>hwJgL1b5>paI{8d^hoJ-@Hq+B{bxd406MNke@BrLc zf5<5pOCHMLQ|91rkw=8$?r$j9+Zqn-)LCF2+x=n>!6XjU%TFHH=++5ugnFRs3gh}8WC&kXz1>H<`U+L3@rgNiya2! zm)|l+Q&)vvipW7}59S+xrEK-@3zSd9%-|g|_^IvDt7g0+WH)202@Z*z8+~ zzW^P?{B0%li^z8m=@(Zs8WDT_4Uz$fO(Ln7+!*cfVI$C>-&*Q`&r;PdRlZ6+09BKv z1dA!Bz)ImERili5PKT_YUL?+bb7S2*eGPwYY~qxvS=tXUFq^t@eaHS1&Gl417N|8@ zH@;ZMN6^8u;XC}=#ZAfiHZq3o!=t5KxnjMNT%7mD$y}LSWFBpjoZGHBzIj0kcz;l_`(7KH$uiu7Cd$|*ON_oq(8>8{WOA4a6_yCByK6Z~Zur-M5+gEIL43(?7B&+!P1-l6qX4VwX=d%w}Cm~`ulWO7T{AlhWB8N{|+LD*wzBXl~dsZ?ha#B2U z13;_%1`UP}_sG8>gVokuJzN+O*KeB*&^u@HW3Y0sFgP+^B@^X3weQGrv`Bl(zl5ji zmFen0ssya=sLaQ32j5Se?#`svQF>{SI!&wqu2>mgS(tYgDM?biTsBU6c1Z~T(laTJ zvT&REQwD2$#Gd$26!6)Y)%5*$Gm+9z!$34-szdw+tUzXzHLT8&^;5Lfiy1w@)E5ZL zxNdV&IGZnfX&@gQ;ZKlEX-VaeJ|#t6@EdJwnNv>o*)<<<{e`BVvBvk>0KezP?~9?N z%Y~4QB4zz2DfV)g4xfAU56PX4Dp3bXi~iEhTT6Yf^7 zjPI>v=q0XAW18~nf=0uAzGsi@O%MgTL?Mx_sf%7Wi5=G2{*oKkaI8ziEqjYu=ova6 zOqT-4;1KmV}U z-%iGB6VPJ8DZUQrkqy`{#pJ?eLudVaDj*pk=3_fbsa___D^M!S4p{jTbdqzegzOy9|!*00tb9X2i>Y>a!{{CS3nNX0j6!hmBnwTUlg>=WoxX;Wb8}8jFUnjctN#Q2r2}aE z>qtOGYEvi#GIcTVSX9Rxx@p2A>$OgC+V$9=!>ZA#@U&}kqv14Dso=zl$j&3K#vwr4 zprOZIw42;yZ-ADgZ1+?iLm)Nia%9Z$qQj!$?6A5K=Z)?G4x@aoV9w;S05YJpk*oPq z2xiWLp`TI(9GPnUJdaIKQcb=tGS|eWmDle`2Efi!8C_~7){L{ zo3iWljZaE^FlCP0#A=zuhh2S!y}UyK>f<+idj6%oS=Y2&b`hfkm@N{i3U zs`2!WNA70d$Ljef-4C4m7Q?;1(GH{OU-*O?jh0Y+!+REk1pX+1^ee?wt=F$LQ=>^1 ztMJ}Uyd_{?1}=r7fhzENgllnC|8A?@{N$JVLT7qru$w>rOyeqj{7~kVOIz5-WyS`2 zi3^ADG{mlimb#QzRa@zxkWvVF#RZ-J+HhmC{fd#pp^!_`{h5~fwsoBF?vMCQTF?8f zuRUmA#6#Rcf#`fF->#WdGq!6G6SHF#)abi6j z%Y!Vqonjuy-GHnP3=)yyafsF7SL!3&T{dKUQ!5s6b%?dVaW&O3c~B@s~1zF zRBZ8qklrcfG0AHaQ@Pt{7Hkj9)tNO4pJJa-b3HcDwUSwPcWgM7uYQgXjJ(Rhe9d21 zH@jEoL9*VtM$17U^oU%3?ry-3pOl$K0_~S}n2hY@yCt@geUZHeOf!u^Eaog4pR&8^ zVLEDNN`010v6*T<86Gr{&7J?Kc{XCZQ&k9ai>Z@mj@*nS?m+hCG=+J8I-b4f$M#Xt z)AAbq0C}f3M&j%1>w>jEq%fX8VQ9#dxy5}8=%YgeL73EA<=LME?s@EM1ROIicHnch zy5y2BaJQS<+_TD;pj%>Mlztb*7rTwm>1#E<1Bo$iOf;r4vRL z-NGk(S(Mz{{OTEgZ=ZT6k1F2NuRR663HzM8j|J?z;I+L=Pn4}ugH}nWdboi`jy@3w zu$r&FOZ*EpUtZZdNqLR-rZe@6>xy0>!x#3x5kXBKE4fauSyeAyZbwSJ^?8H7i=um& z^4%xFDp5qe&b}k7jtt4__^{4J9twye`cj|HxdYP)^>2SgYq+}kOaee~^Z`qNujC1@ zs!>=b*dHL`@NuzcFnNU=f+;BR!Cx-y_B)|j$pHzC{?joCB6Qi^@>+bGMmafzPjJD5 zQhER8d=DFt@Y=$v8Mn)CZ)%x3$z`(L2`X&zq}!v(CEXSAQ|W?KofO2M0F32PPj`nw zaLNbcB-+Dc!$|l5D<~mWm^=hr4XmkSjO4Ax3Yqs4l_tvAF#D-;J`KB|ZuOJ9%U}EU zTm9)aIhBn#F1=F4SY&Dkevy(c;Usq_8v*mAHzT(z;uc4EM^MnxUO6*(VNk1Fp3d0( zQWJo?tSg@L9hcyzYM|s(&aC%x5*$tR&!Z$YIhyjGpF)@KMVd_r=!-db=5{3f(b_i)MdY}-!Vnkf`LgA~;CDm->Y`aH_ zZiQiJJ%v1zLv!$9)AuN{!70c2eb7ORWl)xZU&Bi{>;dG#xqwy6ZLCG|5YJ$4d9|>FxSs zYy@J$ksJj?vs zm(JP0SS=ZCJ7OXa_>cz7>NVd^1utF08lZkrHomjQkAU*F=>j#^ek$SBC1MMo<|<(W z?5QWcy-=cg?C2RPg7sgYd-kPo2)|yq7*}{)i={~p{!O}C`4}kd2U>SUJAXmt zbB|ALUs1tb`u?4%XSgf7lV#^Qvrh*RW(-TM4cID-#BE`hAYr*?9uvky9HOt!98i3b z6B>Vixx|*->4c?D;MLk))p6$}e^@)Zc+m_;)13ySOv8L(dl*!{KeSv9$L<7;Tm-DX zro!1Q`s@Wp(VS0anr<%b(rctI>H3kMNePV5{4QWHXHyMK1-3Z&)IA9M6 z#U{9{-n!@xjmlDF5fcd58#ydiz`eO9q$&~pm8g#7G-T@I*g6E3Fx9OvW<*Tcv8k1Q zXO?Te459Ak$(cDm`-AI;y{h&UFFb5n8~g?*4x;R1&?f0Zwik3u_MkC_j^7}xE`puh3q8=`+I=0-un4QbkwO$-ZaFVb zq*fDXYQ-H;@Rq#UN>VFG`5N}1C(W@RV*;xzl{wcytw1+%%y_g^+iiF^A_X6CvpQZX zadfZZ+-`b&-R!y(P;nSy0>MVVd+ic+DxCPhpwcPY6J%9#ny%GN!u%OURQiP@Y1N89 zb^4MrlNgq#L1MJd4St=c zKZtA>3#0t;&7vct5I@zba(X=3qjPGi!Lb>{HgWYuKPLg{+uNe=4XL@$ViQfVy&ADS zY=pSFpd1Y0_S9b>S}yY9^e?1oKp0I)m&94HP3U#CC(YvWGf=C0n(NHcB3hN$ylRFJ zokS~&pb0GVl|-L{>0AGSR7~r>$FEqX4_7UYtpzr<G&e7fh6ML5=1`kbKDYGLZKkf$ z*Vb4#3n><@b}1-cTt$q<`AGVVXW$##Urp+Jn-9;=X;Cl}qRy@R`D)~XJXuP%O6}!+ zt-lk58)EHF5_xv+(8te3eLQk31=cmqr^}7A_JqxMC$%mXTl3kqg?RDi;&VAd*b>Ml zHYk*9tLdu)DOKJa;4#@Y@4L9ESm(V*lVzOARmrJd+l-$~C0jzj+nN(|8a0_A#V@o%-k5G_aqK0gMr=`hz9^fL7hg;faVX!7q{@=8#8!tb_qLBT zQ+1iJCT*^TyF`dQ}!t>peawo4pi)F9@p6tF1$yu(i6Mee}H^$(DT+QIp%)`7MB@`BK`Z!MwJ*1vZ0tTDLbM zv$o_H+8KD*<-{)!jo}2V5AVpvJ2l|CQd1V{*alDM7PwbgyjIgH0S&)$3z~~UE##KV zRV>ToMmWDU^s3}gd{!+kM2Dr(w~?V#OBS;IhSN;wQi9Oc>e%S8lh}6W=xC6bJO{zg zWXAB&!v^ZIpFJ8b4yH+Wtfvw@DrEaE8hD}v^?aXv_uB@2!~;z?Cd&9rVE+(K+da*-Pu!_>o-POe+c z3#P=KxV=&_uy#&6lSiPi;P$CmCih%7Z(owjdv1MQl#9+tcVqjxfq4%b!q%p}a9j zfi|X>t6IXd$GG8J5Kr{n`PyB_r}BuBD6`39PfqkC;OdUCcQp{FMJNV z394%u7$5ueL*@b1=Le|b3Kdn)pwq2-S{i6Eu6wJg(Q+Y>H@UHgJmiJEOmn_>CwmCy z8c&!M)aSpS6*)qOz~z#vW}ME#R9`lU~1{|YyhjD?TG!lY{P0)SFX+6?WOklJ1t?phm z8|}e|{)G6-sfBuH2DhD_E`&gry&9ZjF|y#xF;k_(4{4|)u&Gu#boKQ;^p0jEY^qH3 zeHAG^;|-)v{!_OaXMa=waxbU#j2Nx3fBWWi-IgdV+BM*{LYD#*3DUSLElHo(Z4zB$ zVx%13w?h$d*(<15G5zZHNtb)Ax0WYCR%W}FXGtkn*zexSCGJ6^4yWa?kHkb}Xd9U5 zdP1X67vFIaqz0#5&~W|fy0wha4$~uZ{GoKe<>FH!wgFI(LdC^k%mnQAeI+tr$OaNR zRw#e%6s%Hxk#UPQVO(Cdq~?_oRt+@UU+^Xo;aQnwAUeYA#;|npPg8Bsc$N}$jE3~% zM0B|GA+~0kbo*@=wa9I~^(r;rD)Y{|JnCj3>)N31)gwEJ7tAt_QfuN(nGD8nx#9-p zJz6jyCcWmrkG$x<1ijf_w(RbYc^+%oJ;SuSH&`;=puVG8V5Cob^kKT**kN3z{o`D> z{>;^iA^z-ZoZtM8)x!jduN0|^XEsKaBh~Bv(P>97blg|Y7*NB~Rt$CvBR8p*Xwr3i z8QAr%(!No`b04@fNOZ&#z=aq+^Hy@853feke#^h z{MwBWNUe`nqS1N@tzfY!(5)FSX0zo<^Y#yHD-l*U3~1^qB~rXAcJt0DH>L~@3uS##D+$ltl11Z?sa zGQqQIw~!@e#Jdv+s8&z*MfK=jwnO1fv?>CZlKaH^GVdNl+Em$S?5wWEC#v7z@@%D- z8bWHXzn#yZFv#Sf*G8JKG52lidt_WZIG~+bXot0~=Ubzn%-7`gr}9e(oNp0cJv%CY zJ`d#GO5bP_*)A5Rxa`A=8AhCsu1-dKT$=^1KUhrtnDi@U_&W=L5)gs->J8urlG&sY zA%hbc`bDX(6&YdR7JbN+vf(U;C9b2@=+fi+_lrF18b?a`(c3!1iK`$EB|}Xbbt^&b zmywNaH|IwzXRulL8X!VojxdQHxp#imjw{%`CwTqSX1VNf5byit0lD7x-qer$?zS5) z79imk4mwN>(kY(8DbUkp{_%j-Qw&FnCL|#KqD}KWPu$o?xV7k>=1EJmnBu(T2eMCv zcCf?WK+5x^g~M|5Tr|v2@1Vd=~`H378;wVRR@_@dcMTc4~Y*hXb$HFY?KR z)!e#{orI?bGSqrF-~48oGY;znY)VIpS}W0Idj^GjlIJ}Xp%Q6)K($0w3P!lUvHjt} zYXKxDj?^14T<~bo%1kiF_N6xKa@fq9XfbAe)C%MA7|}4tuDyKgBI$r$XHEo2H-6+ou$+mAEXb!-hiHLmO3B_ z`iv-i4`KYv6fj=D$?sRiZ`-V7v3?`l4iJ^U_6YZ)TNKklf+D;CvmaH)PQh*(jTI98p}J zPJSAJ#=H)7-pwCP^)?7=46Lv4+{?H)1%xvYcq0qI`ylg(Wzdi}4U{=|MLp4fcPfTe zY1T)Q>Uv1Oum1D^om74AA+Z+jpvl0wsE!@9&--Yg=FQQhzH?=KK*6@)n-1Q4;N%04 z_sR5T>VqZE_aj+7_1d-bfwcab?difqJ1j^KlqnHY^!G_ry+xXkAnjd)I^qJtMuo1Rv$}bfhH+lZ5n(FI?TKni*5zvsK%6P+d zD+XD2j*6ffc3)u~%Cmt6K=h^WI_Neeh-{b{S~%=f%6;;yR`lf+Oz^!=F(A!21H_4s zz~krcBUw@(-H4X6q7^12u0?_LS8deg4wpvLi)T53Z*hi6jGu}C8tg#GnX6wsv0D(# zjyJ6*!=p}@`s=Kw zy1|*_xW4dYPi>21SYOwJsE0UZjULk;zL2w47Tsru=cki-J!TwPIJb*dxb&az;U5U- zyTr4F?i)S(a&czJ=iD(udIV%5YNEv~6_bK^VT~cA#yTrd>N-LqEPnz!?B#BhVb6}f zWKp(@Am7?O7qv)38~a5WNf4(%!_vN$`@H{>2m8fIO?25}{p>5xT^7;(V@X=T+#h|h zI{tYT^%BZ_eKPwEb6+#2^rY%_nJRINM#&?`%Uo7_{m(D6dNY^3t!j?eXG zFA7HG*h5{%eZ(OkezOb}78CMMy*?Upn3&$YKUbpfH2i%A9lAY0aWgsmwf7f(O|NO1 zPUe1t+tC`Z@)YY!gj2rTwlHI3c|cs;tgaK+&g8l1WxCZkA{Sssf%W1Uds{6!zhe@W^)NVT`}S3oJMz()WCSrklIq@0%TSy&Eqh*JZ3SgWWKE-g}yVoVr*fbX-w4E zAKwB8a(za$_d_({#{0}TTbIl3bIZdEHPKHr&R#P*)|>`yyl~kF^J~lT{Mh5YX%!;x zU#)>_u^OE-j`ycpqJJ>e%;k?lowi?&11WxT^tP||5oa@1md0bbylB|jtniz{rd>el zWaDM+HY6MOw&jngiLPwcEYMQNm#qOd# zVr6>P)Tqb5Yk)AN4qd8E~HLy&rN(=0dN=HETJau zP3qS7Z)T~Y(MyPSR&I7zSy=#0NC1Ai;e#sjJrL7|T=J z(xjVs!MD2Z0xXi6qCl8N0jxe$Lh@ZDLjx?Ym+hiSx*h!)VjUBPFc zWE90;>%KP-Df~)a%|Zg|peGoT)WdSlFxuxQEFfQMFy}z6Smc@Wti4#k@Vrudy`OL( zZZQ-YN(T1K203=%(nWVM$_I_n^w!yamDS_RCPJAr<6YDjY!tngeSr6h%zX=`?s%lN z{C#Vh1Ck%v>xmR3pP~c<>s;0EadwwC(;z&OS$ng{w{E6~(A%Zq7|0?S0vpjv%`6gd z|8b?vTq2wIxX1vo^ful!jj)mhq`9%Y@E6~I>@0exQU5lf%PvvP`%>-#%HH+m;yE7< z+!sf2F>oIF1_3i+3jc!5eNV1#YO?5lJfhF^@)w~^!@<3?cfp9=iK6I(UTFdr(;MQq zxg|w(@yvQA*v5nI3cN*;d^u>Z?2><-izViY9RbT{Q<<7Tk&j)=Tw53})3i$!zrA}X zd}APYvm(PH^<6r4J?0$_eW<4BU5wTK3HGW=uhpAFDQGr?21>&TuGMwn7>+ zG9S*=z>?54QK-eBgDP_Ny2~jl>YEMjD=);js4=kL=k5iOVG3*OklRWw-` zZ{6(@I}dkr;sCtxMjE}H+uE|Yhr449#fUX&zzWM6587GbQ~ zlr3bfVn)sM@w_l+zd*!Ww-jI3%8rhk6K6e(WliqbYZ}bDz51$2oKa6dNlwpPN@vNNwOM8ci`PvykI=K`~ zG&clsd(3L>N<#70(3^fURqeFAt8Yk-Mg69AX4cK`qst}cT(+QzT1FmaQbIUK%9S`j zXoXfC{XB0pto4|74qq^xm02aHj{ZKf{ncD#upNS}=>yi1=UaGQTB<)}@hXOCo9}{Q zd1JmOdhPmxOx4Is0@zXfRy1Ir=6hAc`$~Abez$mdC8(kVk3JQ0Vou$`m(57oWhTQP zv>ycjnbA1s)0lCqbGZn@eJiEP!WhW zyZV@b?cIympu4+XwWD3AXgNRVJGl)mxvW$8je7&N?fgD}a@~{it|3r|smP{WeB!-O zE;AIT7F%S|&~=M_lJcGc?xeBTpS*MAIK(gUyg>X(P2`KmAD-b*n03d-(^{EhQS1$H zg5n`?h!C_Gl;_Wg>4E85a)IELE3Wk@HQ8iWZ!Wy|@Ht%|se8}@DoR{IfqGGOfuC6< zhqMFS1hg9-MGHNE+G7gFC+(G_X29K}1A z{^Vq=SVbbVx738gkoTIVxb8%o>G@&?V^Mp+u5GP+alJ)ux;19`yO@XZf)l4E14%uW ztNPKy7Np)cS4iU`g2Lt3Xh+`BG;(Y2*Ha9;KQoMr`0kXcle`wZ&W5XETNOiL?u}ji z)gD2{*X@SX4J<wA|4Q;SNmz=g@pc(2a^n6FrZ zkG7~bJQ?wLPo`}01t)v9x=2UWuVmtx#}Pb4A=ERrR%|}#gpmR~+5(Dkr%fSfIKgo1&A z?~}zcF!@PxJA%}?<8B6RDxE`cQrIV%d>o{|n(`Ive&-v5q&M5H4>oZG7Mjk>A_B#s z_|t$Z(*Z+|{0YBMWM4Q_!;1+g@iyi^@~$xXda}tA-Y2xpH;?z;gVL_=iZ4!0GVNA= z#@DkgFR}lENz2*+f*;&FLf=qZkm+~1KLku{Ir|5jO-r0u@JSF!)dTT)@wIR@vR?P` z?cyPVSy5X%*;yDdNwf326+d(w6F#%RjGz{4@InF0Cw;*}y7H$(4Ga1u8||JO4TmNo zFBpq~AhZzK@#*Oftq_`agKMzW{CT8<(wxYCbTT|gQ;{@?6v!1(MlwSx5!s95Q+R6e z%sQ`}cgJEqW0DOq7s@_a10bieY+WyI^J;k0>q(Tdts%i1D$Cec;_3okyFs*M3!6A3xZ~os_1pLAmM)Kuxt}n`O%#WLVUh?9_VjOx|B;7RuAmtuA z(j}ozhc|+wYH$SESB@}Ih$bd4^7(%-s1>i}BnX!w9f={v+puu2oTI7CoB+x6gCBYqp@DeRQf4C4j zjVn-n37wdl73MK+XD)JbiLPKc9C$V0hUHamqKvGX#PBrD`?*t^YCpLg-S1{M6 zC}eM4Z^T=E7-JCs7+=98xXGC|D|QX}V9}HuS-WRC-BcmOd}Zs&edC|GuX%aQ@Yt>2 zQzzRio4{bj@`q!yIuQ4_sqv%O{M70kw}RMYicF&Pev=Im_9!6=u}ip^PpnpD9D@4@TmC8`*Vvh;&L=ueYvhRem<+S^uGD+KEo zds*RRc0wMGw&mw~a}8JJ1_A`#>Zrl@H#)z}YgQA>|IN!0QUY4Fnk}1RId{XN+B zxOh-t7fS+XKJkT!QM2dZz zMpY}C&~)hEs$7&Q*d8c3H$q`OS}iAR$b0VpI>)iabd+zB{mNfsTvH6J!W{Q)){&vN z^AJoRqm!6GbS_rwBTn$)aRkn)5e1&%eKcPZ3W+O?^aubg`3)e3ic#IMKb>%7^yMOr z-|`A@F{RB6?ip<03JiL{xjU?z)hd(1THt$SsReygE z1B3B@%YKlvzOCm|*NB7qrpTNk%{~u=Bvdn$)jI@JOeZYTRMLav^UG5$UF+FSjDXtM zG`=f-?u%Tu3d6Cdp=>QFe4Srmt(LwIQ{`$@hq|Ll-q-C@(-_Gc{F1mcv^r4X=1rXt ziq?Qd*UGP{44$nQ^}!un)>F#vFgXgTc5=i0+Qxg1c0kfPh+os1H=@l6QR zpqkvit;^_Ar(4Nxxs$ij{eS-TtZIhSmMe? ziFm)-Y*_cEG*}h4H)-jTz$M~G0dw=!+pyAnoL0S(hquKmR_6xEh9~(td3)wCRKt!^0BXE=t0U* zqVl-F?tnaMSIsVkg2!o{sTWOy&A6f|BXckYN@j98jv(LLi{LHJk~_`Y`m? zPJ~s$Qz`7m?0{ElqM>YoE8%h@VXO3NVwE#(J96>T>nPFP?G`cjfB%9afoiZ`8{~c0 zi{R9RtadU~Cr>= za!V*GH=4xZ|5>Fo(KA)jW!w~-&8Tu5M(ErXW6OtmJ)2S|e0iG+X$Ci7}$=p+1YW>I4Npho5*591W!tJ%Hp}b~d zV9jb3EAXh)brt@2cz5dM6HQ7iG1@!$gG}d74Vc}xN22adWXoHsYNE6G(<%bg#p5dl zGPkq7GM8*30dJ)U-rVG|JoT=KzO3l+WOeM$r3X(bJCmeZFRh_B8}{KEd@HV81uk~M zIZd)FiIVz_8z2?3hDZSY@dnc@{!rTnH=!C}nE+0GU(v9EXN3&t>c0K}Z4nH!*8i|f z+N6yXfIpgRuQ0s-8(W&%aE%qkkx)pJ6`22KU-vaz5%GE)`KI{;_y8gOBlCh zrl6)LJw|o(%OY{L@Q~EhneQ+;H8lTtrgs7K>xovtu==vsam^dTIn2{--H2|k&AVMK0T5gtOTOhM! z(5LLZ?DW&GAGtfgXqdk+O-fvXc4^)hprdJ55WfVx)Hyz&6e+c@F+Ry8br*X~eHaGB zsvuOnp)x@zS)WVaV^8#u{ew0W?mxW6KRHE7Hq=K0BYO{fGGPE0Q{_pC-pkcy&w0}E zuj`KMI~v8Yw+KL98NjC)3Ew&2dT@m!D90K%g7WjT1cywh<=xgEe>YV z!V}xH{tyQsW6`56fk;^};WnH{onJ_{H~8qo%L)E%DQ*kpTS#N4g_QYl6^A%H0C+U^ zB0rlv`tj=Okm0x6iM&lz3ZR#%PDSiANV=00b>lBhOd5@mK0>JyP5@Gu2Ww3f>AeWiEa7pLr;baRCgg z1=LjXIpysGkVj*#B|6KTye**7?cmk@#D$f^6J z6EK>rpgT_%vt}y9L+Vqj$2Ww{aJ|-ZEoHn|2$n~yZ}y)4+irc^-G!bL($UdLt}c{W zQYrFa3Qc*LwcMSVxdcGM-5cID{tsTD*5UWeatKHQYnMXUD-SK+ex6aYlo}6FqZVY* zkDo1r1X?XrYfV^z82Tnyq6IassBh6gY(zKDl}p$%EdpLSc*Ud4*mQC*4e~X_+akS6>n7BDvq(=+nN6M?Q{kiCpH8B!iziNQ)ke zN{n3NS)N*+x~OIQY_(O~T!U^8i@s9bSA~=VF%c5^(Kh!yk%%X=Ov5C-&}u7z~w5iwC#16Z<_&>cq>)QuHYbhe?PlxClgkbLqA&>s#_ z(x+um4{E$&TjBIDuYpB2J(N)z^?TeMv|j+dPp|p?!1ego662XmGJ+?I6y;rtqa$G1LXTY;RjCQP+ZSq<#88z)!pdC{!gU$ry;LH_OSr1HKxAJ zpO;LUoK=8dfNHe$BnJ2ZzCXZBj?GskY`}AY%~3#H;1jdxaZDQWBz4vWlNaV%wFQr4 zoFty~Rq{({RZ%i`%*+=&>Abb~Ta!;BFjMp<+x}Tx^kkm4iCydb>quxOB&kVb?+Nhf z37*eX1UFzDN0*=jrQn?sM$^akPN%Q_Plf5<_0tFrKof{!`J(gjm3b z-ABRPaCHV)7}D}IfI|W3g$}?*?k~e2NpjqeJSTROZ=mXvj{Ry%!X1AHT?>68;415~ zF9)FhXGbLghyB=hMD%9oZqx<-~GM9|r{Nb^H{Z`+@q01f9i=VfZ0K$i ztNC36@jpB)-d*3X1tm@51^Y9xd|58*>P>eD8tS>*EEQTw_f4)3y5ON1+9to(f$uTp zuZ5V%%I}bKL>T`qB;I$A@(yp!sD2JZ+^f6e1cr)ON50! zXkZArQxpnlzCTlF+#`Qyre z4m` zrg8=YehKTj)Nf))pAC>Um)+O@;WBuHD1B{udAr=5{UlFdz&`pGY^J9nI4IA;d!iF-^v&V0!K!Ii<)=osDJ46LX^}2#r_{Q{qLge-`^Lar^UmYTb2PO zS77YT6*FRV@@dorqKgLxgN_pVqg}%tuhL z|Gh)~%ZmKxH^s?um3hQm8UrD|1haQDq4o^6!TSMkZiarK{dNw-Uu|hTay&bFvf4MF z_7vyC)u8(psr~p%1SC>Jsv@**`T9w9^{?GX=#;qOf-T1WosliYTAXEcW@Vcre|`Q!mg z{2|T(|L(g%l>f!jP;U>^$raAq?Tu|Xp|R~i`dGRJPD4=T%IN+FOZk8c^y1>cyDH%C zb(<~udbZ1?L3EV^a}}mzFaC6b&3`d_KwBe8PPAUtUF?kTsRgd!{~cQGGI`(Qqg)x+ z|5h5{_zg*U4bxi8+Al+8MYHDXsrMs-79d=l`QA`v3=whxo9$NP*}~UT z!)UZH{+Ch+_`bvj7Zrr9nxVb0l zB7A-pg92Q3ZYsbuchtk-WZp6^g1uk92aU)F>;>RuDzDgLDMh7(l$Sj2k?;OeZmExz zu?O8#%@i{pP{+JlghMtP9bqZZp*l9OiGad9_h!Q;Kl~zfh#xtac;uP3*;9lO@$n8Y zh@oH#-9g3k`A;uZMNpZuJj7JuOPQnQnX5|s#%GofYb}ZW{?m{CeAO1hpfx+Nj=XV= zrHj_u80cZyyrUu3U%2z_MS>_4W7_$%8O!kpR#V@TiT~%a{^Pd3ujJF~ot%^F`6t%) zNcErS_%Z$WC;jc$La{W8diC|VG-TAk&Xs=1A?bSC{l8XILg_(5>2* z`d_Tr++MW&k_fJH+V|;phi@L8qS?{?HH-MIYS)g<9-TdXD)7}~rSY}e^nWy(}zoWt}c zUxxb6b?}JCm)p@BEc(J49^80Vt{%=|C(aZa_jp-gYTZtOv6fJNp}Ccj;xh~C>mH5GL(o=cf9eym`UvQ%audCeMq_v3KS;Jj)%?x;I7O`EPFr>==J*p+}g$Y2Me< zVNYMTHE#4L5|XIhTwe{mBeq@x0lzh>Y+%#?z>Fq<%nb%w^t!N?oxFfSONeyoy-~!8@D14dT7(c5_LTh<_WFhKSZbduPQctgX~)c=*vbmf*eX^`So7 zT~f^w7tf8_^8gP)-F8ih6N?jxGC{~el3uE>sgN?aMjDq8+|A*%W2dY13_r|1TN!E!;q*Y`xk zYI$EFE?PoU`w$+n_Ed^Z3h~zQvR7ONK=Ds=J(D@ z-z$FI9C?z8&gYhE&UEmGqljex&yn%~e>Fa9K>AUib7tF&Wl2iqd`PV*90|bMR~Nvq zfzlSuH{Q6+T5Hhc9PJd8Bmq>mx+dnPQ$se_UrTScHBz<&Hi6t_PBPEqsNuCIZhf4V z6a8i@_Dv;MVzhNY^0DJ`01?ehtmCvvvUQxY-K1EsE-m)jZbdpAU61H|a10z(EH18) zt5sn#_+o@47i&8S4*}$f)M_Ah#^nIEApcy zT9CG68zeky#Qks$X7HvbugFU5;5`P2T!AKHvtWMd>8lI(n;ZuLED(`<*=kZ{t& z4;F%#zx=ixXP(gm$^A{A^7J5V6$2bP$)db33|#;djX{_WBw5=u8`Tz6>=qXd1l+~Q zDoiuWY2Y>CyOPb+Gw-vQn4_~!@lZ?Pv`yU}sY)2=& zCIXX!qW0ACrWSe^WT>(Umbg_tM`C!xKp^W2NltdDEF~$s5Oe!yM_JPfMrt*EwcaTO*4Uh#I zo1YRXcs(@-3<1L&fNVPM7j)@-w?yGNw?Z0;+}sRci*xV>020kUhGgd9Qvb8#z72Zb zpdJhvtSTtf%|M)9dBN5lYh<$G2hRA)kB4kmBa!O?zjSLpAx)8eTko;QTIV4J5?zVw zA!5#M?sR+RKOG*=dJ^}{ATA2p99loJ4ESRa8p$KZHS~c(?vbd(S%~6>EC2~*s%nNO z^8nM;Bt8Yqft+)UA8tPuI~i3F6rWWE4UT9Rs59xMZOg)G_aN!k= z?~p*6C5=L&jX(t`AW@Sr`R@6z!N5eCaAITS6dpJ7&wDf_YO4`Ls+9f+r7w=D?=%R=n|bZjsyFVlHlFeSM3w>N>y) z<3$!LKA;e1@o8Ux6J zc{s1!G1e-)FD9jo@c)KTLg>_=_qYq?H#e?liCKzidH9=$Sg3PpElU)|kk6%KwM4AI zrWy$$P*TO%iI_BgkR6M4GLL}%{-@W~(Xz?gjb*=p+%cl!cAZpQ@)JC?`WcDOhjhF3s}dzC&Nr7VLA1`uKLW=)G*l`G+9x2USb;9=k;~VK=at8mopW7zSgM%0du9)nnszAhpU7 z^wrB-AU@6WBoUj8*%uwB@ZgNw2$pE#uJvP{{L*0|~Z5ib}L_NuVeN_`{tlWtdcDMou z81S81@ZT2S`oy>#vR|8hCnQL3VI*mxdJcl7~EGHPj;k}mni}(b+ z?Z56%vabtH9yU#YL6>Xy5z_@4ES%4zH;azR!Hb??nw(f?67lC#X5U66&U!V*J{Ky` z!R}mn5wpJcsRQ~EU?HBk26&S1((~6MpyOA%W<$4Tx(fJ)X@=41Ge+9pF^iGdW*I9PoAhZQK)>g^g9*`%P@c9FA!!X zmDwxLC78~BiOj&;{yH)G~_TSfW> zawy+>uf2@>T?W3X0I~s+o{KRiL5lF+5N4?7EGNZyinfA()KV}yKd{z!Za1MN7QO)uTi35MZusg&tZ799tMQ@qfU1?ol~udT zrh`JZ7eQh}#HbcPQXT*j(^kATd;2(Lqrs*AAEq+`Nq77a?|Xgtt?wtkHtI|;isYYY zg!dkJ0U^#aaf9x_vt0%23nUxam1J#f%QZj{s^0(-2kmP}_GFO5At&UKdRW8Y1-G2x ztVhB9(pQQgngd<>J8R}CR?nJ;B<@ObDhZtpBYMXUqa^eQBO0cP5df{5iswZmZPKHW zdS^@I)i|wA_d^ado2la%wG4r5JJV0NWH7!3PtQ2K z+%U9-(=83hU8B(XJbs>9tr3+N^6Sx(g1Ku}QuZdkO`@Zv%J13x^2FS&dTIQN*Mc`R zB=>m#Y${PgO=<1YP8yXXrwzZ>nd)gHbDk-#tfS~}Wq-8{mSr1Sl{468s~V|J-aEaE z!^sv^$}uLV>Jyv$(UF-*-PjmAQ91eO#2I!GJGGb-(-8(Mq+JHT@9o+iDS^forB-? z$YvCJHZ7GOo=3;$wxRU7ROgwjv`@)sMr%Nca@Mcv^i$E1>EP{^A5j=%Jqc1@5!_ii z)mDjx+5wF5xpXR$p6kUpeUE5#M4Dtpn#8)&r(hRRzfQNlMe^W+@PWZe?6#|(GFVQ_Q;eCe?kR^@mPk#Z;=Ak6pxZk9bCVc5PIaE zDJXGGN_&rB$t$KRt?v1qriVYZ$NdPw!`#Ko%|auXU)xqauo{9rob(K7>+Y*iFkA*$ zFjkMW1r|hAGYTG;YYtm~$n$U6d-u7%8chwpU!Z#wj7E!fKp-Y?`AamuPrJ=BVPWBb z@=-F6^G@vx*ftt6J;&9UaITT{g$tJ(RiAi(Tj?J&p?Qq>`!j{n8PN={8=#<3x%MaeQ@4omTBIA8%_l5 zwH#s&j&+#(J>;M}81eVbN%NXvr_8ATEVulF9bYUv7d1~d^)#fH)pgO)u_&A&~Ql&M10^a)B>Y2&|hFKjWbFMc= zOl6xXok@&t-G%z4+MfLNEZI%cO;PhZ`5d=YPxAANkd^8$?B%WQmI-;B@B}N)^*=Q? zx^xR95hE~Xv-Io@)|ulTzsXOkwoQnLMX6vjOr-aQ2yc?m)Tsnh0CpYoY61w@YMema zk1fZ_zFnPdt}|Ccr4c^R3dG^q-92Do;Q9WjD8JjIbxUR8WDW>)Za@F@J>-^911vonRilhuNp=0XQ5D81~J!~4vSt=HIRIN{O>G!gpZ_IDjOK-i0hS;nS zWH)rIJMQh4&`g8ijBE=7Ob0b;sn}9tGa+=T7>g?8)k_tBe_6SO*?JIyGf8BL;ngfZ zaxQv%*?iZ$JXo15rt|Ie%VGc6k;-QqW#^al;sGPE-7)22dBg)OYUfKn(kA9ZH#d+)W^Uh7)dx|Wv+jOb{6O7bqrf$;VQnZXLf&n{!8)2-J-`DZMg z{I71X7E^6&O;(y-6n&V;Ft(20C`_DqP@ZtD>G>~K0i#VZEZL0;9;c!vVHnP%q@->%cZm}K>7A>3Cn5}Vg z8Om8d84h~ON6wQf7spO6z%8TLLR-;yS`UGkK^j#f zD)+`vT_}4PCkd?zZ5o-N2rE67pE+L7R#6VmnC1|IV#9W4RKt=!s9GWJf?ljMqK)?r z{!2kA1`ESU6^4UP&ydwJxful{u0@rvKASL`)9EWb-}Zg|qWusgB|*;DNAZ&kUt*Ih zzqYWpoTw|ReW`!93f+m`@+lb>%O8wm&|`WnN*mAc;^FLRwRM=0N33ClwQ5!MRE+Z^Gg#5SuN>Wl1aKE`!p;vEP+&1Qp7am_ zv-PprLWK`KPiX$|d`&E3#!mm0)DIl?sq1$*zrpaj_LP#Qh=rC?nN#7`3$2;3y;Av& zQoRmhK9gYf$g3}2yA<^+9k4VD`Fck28D}oP*fvxbRunuygTx;Qww`n>QOZ{ii(y&{ zp-X1jwg&MdVn1#*u=`f@`OOu6#EphH#BRKV)%1bcM7GPZhP69`RS1W8fGieKduhx7 zB!O*ER3;VCuB15)jLp0}DOwZ9%I#u(M4DY;wXS|?3XVXNv~u2#5<+|lUOL>q0tt+b zo2e?68LLfu)6OZITnsc7?jn*ejut}rVU8|u@n+Upn5qNczICY? zQ*b`~$am^-B?sbIJH4gi6Ifl>cC_1;93wB}zi2pZlpdUqRwC zDRXQk+N1dsFlvdnjx_R#VFW>(V7P#7jno0nt|vA!@WaB^_%6rZ!Tfh#*X0`q6Q$eT zKkvEaLmGQGXDu2_shH27|A9eEB4S2x$LRBN1pCwqe$)|jsxIpXj>mwrPR*L#ma-T2@;OO9s5-yQ#-6JggQC0t;A9|T%WmbWJvB=k4%yPf4G5Y zUixeqHnnT<>Am<@>3B8y0`vFG_tR78T?oAFKNq+(3BIugLo}^t0@Lrlxt&RhKLfAP zYII+Ycq8|?#gCv0-JTrN#ojnwf*0@|PY@c{=aVMsL?Pg0HK{O1oGKA?`^U|&b~p$- zPg?NZ*MtL6>$BQ^_g;Te{ZSXmOGBNx4Msh?@n#xBTtIeZuP-$3#amj>AWc`D*O=%y zGvWJ1^XAm#Yy^RTx4Xw-1nb0i9>gm0cyeZ7VT| z$#83`7L20!T;+Cb8RmZ>d|unBs{c#PSK$h7oiLvYS9lxjt%j@7@zLWY7Xcm_9+xpz zbA-^hk3RGfesDMAAKZ>;y4i)4b3Cbco7KAmVbl(4*kz;Yrs7o<_u0|fEf#kADA!)U zIU(00$TFy_*t@MhR|E*hv~z=d&~57?HzSy3LmWW?LSFd1nU8E6Y|w8rk|)@>h4rKgk9hPkNenv5{7@sm-`f|!&NF>LKG;{{<$OT>Ckz5YxQ z9+@MPniNSg?IPPHt*Ya&li^YZYDS}pm8c9;(boO7p%+almGzD`9460eThD#)A?(D6 z797PwZVV|#8q>iq{`_lS!ZmS@71LkF0v|IbJ7_kP863-P{22@{a?=!q#W2Q+?RInZ z>FrOfPuG-%trjw20$ORBOE$76O!pds&7v+57n13N`ss8D z0=g!?w@Q!Q{5JMh8(Tv7mt)LoJF5!|VHro+c$xlJ0ytA=Z8?+Yzc42}TT$b|xlX+o z3|kqRO~^Xgy~oSPVs>%h&ey{y-*_1Qw(KR(5W2ed&@qmRCaV1L7!RLm5!pmlulk6z zN-_QHIVsU(^!ZU0$-xqlM~@qRPhX>WXSAzU!c#hJOj6OmO6#Opp8VEcE^ISfv5RK% z_N91_sJVk*6&<18C31|JpD`lNE)2ihv*(jPIyWdmQ5iIrE92tit5dQj42&x&!a$tS^?z)7pS)h_!7?FrPSe zvn8lW8hk^vgge_L4#K0~S|Sd*N5-z(5Ky%R$G@X~-Jo*~Vq4MEIKlSAdqcCe^)cal zkqca!%9W5^0CJL%5x+i2+xEbXI}yx}{`Q#t>pJ=qT2y}ICEKaj{SzR``MK18?!s=~ zwev%OLvNDcGv;dj*TO+hJBbpy_by{6nNyc1L65V82ht_rv*D;^2pI4p#XXhthEbIv zWc?CQ`FIY1K#Di=Gu5cW-cfGY3&mUeaN+n3W;MQ`)9*QNvhMmPqRhxJVFxCfoUj97 zR68A8V;Ms}rCtAXgT?kRxwAtIw=>wrUV8uBnhc3dveP^+fcR%N=5x(`3oQrIdL38!c)Nt+Y zp1riKV3I4UgRRr|KE#MPh~fS;2sMx=IEO!X+o<8I_(k=HaV$c+-$-PE%#4?^T3?vR z`ANX~hq)v=p_?oU7#>9@eZV3MVo^GJUOIif8gVZV?l*4t@n;s+9F%-Fn3NWN8iIZk zGTzBOQMp+E^#et|_9&FAq#r5oG9gd&+-*cS4@`m1=ZqiK9CGFU>9~jJBWgm7>Hg{% ztFiVqd5xBSAdsig)l}>kr*6m4S|91W#O%VtFQwA&Lvpv6^9eJtC1>_SUlI7V6u*O` z7MlWayAzHR4xBWQc-(m&(P^PR-+6*u=BoVMR`+tvxI_zw9!u!`5#}4YS#l~;(BX8pxOGP4AdBk{ zCx?JK)cdvJnm{oAq3WQw5joj3!TUD!fM;zURBs)}MuP6hQ*2+&XfjwYU?cbmaWpmW z-4_1pYP8432C41d7s?1ZaGsj9dp3_VRG=|NOr?Ak*ZcYN6g(11hD!WbvlO~-OnriL z%DLEEsN{tv;Q%yGbj%lEpx=L`7ExUo-E7#Gz&{j;JyfvjSWQs0(YBaAaI{q%Mf`+5 zNqTs{S*Ntp8=BpLBwKnsU7%5AM6xwmVapFozv>xz75|nGa)81}=tr&(QV)G=%Df5^ zi;D!KuH>Ewkx8v4@|wp@u}4Wg`=aXl$q4ph<$kp(XDWkjcRU~a-A=)^cNouH#~o!a zhQ;0w#t4j)^=gqQ@FW4PI=EQ-ULgejGz^l|A8>u3-o;UMJI=JwdS*LgO>5>@C+jhj zV-+&+K+3?G8pLdcK1l~1pE@efKtkyl&QdDKHg>{eIx6FweL*$6Ld*I_?>%5xKh3O3 zA%}6Odo}jzFf&f<*n77z=ERk%VZa%tSD(Ea^JW<4H5>1j{n~$!<0f^S+SsPo6VJz2 zHS6xu$vY9t@A>Yo=XS#>+a6{Pt|taP$427POYOxady7U<8OM!@!ssftQ*#p&bG4Rf z)T&PW)VZVWVKHpEDK9SW5nphy)9MDJhLn<4`F`IjQdRiqyWNc8n{HvS*J z)ZL43{nLrX3G}Z1l^gtlA&%udu8Fk8=<}AC>iDBz^IZR}TJVU9W=P~XOVT(M0rLdr z@R9R`*+#1DQ!oU|t(G~`#H0VTMgwEp!K0LzmA<;GHTYg~$z0)yD%3vsmT^Jgfmx&W zc>foIlp)^GOfRZVW=1#hlG6f5IQi3BN8w(fKP)U-Vk&vq8;(B5a%iWmA@Z^&+)uBO zE!lnk^k(1&vU;I6<~-gK3c`MYc35`Xrew)LyHIGt6YB(1)1ySk8o!mmTW;pV7muoD z3>bo_O#vPen|9R!gojJwG?C*_sBW&=IkTNThoRlh8L7HJ*gf~7AIrYvP|0!@cKurU z@VO+pRUD+eLu0l-*7I$2O7<;bCm2ts3!L?lA`_}~Qy8KZcg8@Z#t>EGvm!s{cPO3G ztzv~vP2y4HRt=Bdxnr21y`2NKlQlxM05*c9+>;+ZKC;Z;906!y7MDRc?GqXeb?z`C z_7ezOx~g?|Xcze0s67LfUMEv4@vMZ_ESE%c0 z2CVUt3vpk?z5gM;FLvOi?_6~DAazhP+~iHTq);h|1U3Pc*%0i8ZCJdW;3^q+Vhfli zW_IoR?RTJryfw!*D*c@1s6L^{2%2%5%1@NW6vLLh&`QT;D|n1EdVjhV%-2G>Th1n$ z#zj@08L-q=nN_=w>cFpM6z}6zK4d>?%$KP5vByPfn5&$fmk(U)m1o+po%`dL9HNwt zFqx%ZdEqlUM4vAXwb5aqnQMUAITecPeNfrfxy?KpG#a22i?dLZAYJNLT2rok=TjK= zfi!#lrkpsG1OjIukZ@|J#g6Y{Yi!O}D;eyrNRk9%tIv(*?YAc5C$JOR;=!~y1dKeA zZ|b@ChnTS~%wanQc~^0){_2oNor$+cD^3FrENVR#F)eUVLeH|#JuS}D@Uh1=lP#;U ztx$7G0>*WFywZH5qBFBA!*9A{9_to-Mb#UdQ!`O}h&tLD`MsFN>@#aCz+PTSa;dy= z;2Z>IN4A;W9tVv*J}(Hi-j63el3CB9-#K2{8yro=z@iD|zkav zMbD|oJid}N?oDv+kryI29!`3TMrYx76}XO~I2u5aD@b7B6uqrwQi4cM_Ys2iZ)9{t z0rCi6Cw2p1Cc&BhXrSskBkn&<$;L-n(Y<}}$?RlgRzja2#u8t0b%WVFj!)LEiAG@n zcOO}r@zw*u6C^NEA!ESvQ?*5N#BfRv{wC_D?u&NegU*n`-nSBCj~xYegWvA-Brg|) zcyPgaS8OAQsh<6d6(~Ya-(T355W|&c6ef;PQuIgW6|7Rdm9mR&x9HM@I&NT6KMdd> z5S{L)Uep#42o|oA@lk+PsnQdmaY-0oz92@{nwq|;vPg6?8!#6P8g5DSCt<89Em>&Q zXRStWkMT`@(#mOgY5&HhY?5se%?973>};W&buf+&Y`B;d(z|$I&eizy5T2KA$`Zc8 zV(u+YMtbu+!SHd)X*7nacF?=wb8WUpV zf`BuJSSJg)c|jz=IMB=(|Ng5MP@c)$3ZBIY`x^3tn}b(LdR>V z@ivl}!uO<9B+T~lrlzwD$g$6b^Odgy!3k4iXVGbdFM~O_CritQ<0#|lFEz6=)n{NJdHNEdsqjTZoL^0Hd_diNvXRe~)~4(ed2=+r5day+ zez|9J+VOR9fT}{d2S^t+netE9z7dm9$O+Z>!3I8XCJLU4$7>ys*&dUr#^n{&mWl=< zQD%L9uY%TWoK{@9W#c2?e_ebyq}!V&z4Gbtlt1r-LcS73#s`&M!!N_}38zU)IxATt zR|_daclp#OD&@}?u^u?hzzoey2u7j|b?Y2;!I1G_a(dnwrv;;Qx6?zO%fxF+C23o8 z^BajgH@HM{_IBKWW*P||a$Q5aPXFFB2bm3`#H}Zn;I;ze!12YOaMgD*)uSFS2-JQ= zdAXCN$Zp}y?Y$${Eo}nLv2Jkh88sX)Q!s8adpMo(teg(d@^qH4y_wlg5_CC%yk#eN zw#h;9c=aGba2W7_vhq*!KVmaYigBcgsjVoqW8^nsl?mebv;lY=WNSjLU~?^U#DaH&O`{auzk3 zx0GD++a71#CLPvwcrdQ;q&F)WHfo2AfTl=UIXx%Oz$-}Z^JI}(=U+MG*{(Y++%N#3 zpZ(0{t{bz>_j^^dCZN)phHnn&jUiLmVp?%i7WEVDWtU9)L6R6f!Kr$q!bv|vn#xlZ zf^FBBV>2}|D^&^;7Lt+~>OM|>PrWpkS&9`3G4$fJRGEY*yl$Kf?=4j$$dY0s_5@}~ ziKqPpRpd-Yx<>69M^j&5b6wVm058E7#JUq7*;mBk!*06=$^N*nEv9m}iD%NtD{`qT znvv-dB&;<_e#oa@Jn+^X-1jcX8pBb@IWT&*tQxdIp>keSVhlg;*0;|5X93Sd#{BEP z)7MIGzGiZP)owr;XO!fjZJfSPRiA`GHCLoP0vpV1Yv#$RJE+lY$+#m=OO@BBYo#M= z+?w5Y&>hPqJtQ`%dw@up-mqQiwLeDMc31-Tm@WB;X=~7(mq|x>d4(ICGj_9u+*lXe zj2X9arnq+{XZxfoQW{menr{MH< zQTUaGju-JhA<-*oQJtL~JA3YlPabQV_KVIomW|2ErX^LE5XT!2QC6=_eNDhP156n>+%KSW!(FYdC6SOlwSoJv8NaMMMEE7DnBLTIWOf2* zK8)U6B4i@K*iBueuQa5K>FR>C9a-(GzZ96=wIf(K7qcVq)>$@h-&4PY0~tt~1mAWM z8`4K~M!2ZKKH8Q5I>I*rz-Ey<3au&=FbfetN%Fl?n8~zkJg+oWxgYcGYq4yjW0q_e z+lW}{V~D~JKyw-H9KaU3 z<#QzicNGDzQe-oUWk~%*Ej(jZ1(^__QC)w3$=qFHr51E9SA3dy32f1_AVEIWh95vVI$llL;H^g0D~abxZdG+-2S#+9cH`js1V9!-+SUUcEFyM} zYWaxewFW@OEbXeLo`r>=xxkPMPRFI} zWFubMc_c7L#j?x3yV#Z@=1P^Dw)=^GNl67LZtktp5lo)t z8r~IW?SCP$WXw|Fwm;A|D!scf$lJ0@$fSlrJnhZUG#$9ogfbiLSW3E4uy0R<7~;y! z=Xszh8_W6KH!!-I*KSn-jhD0yk_G)sXyr?4o40)#Bzl}8%4&@fTkqM2aJi=sr(UET z(920tc7Hq7NU9+&1xW|x!QjXjeAvNq+%1}?p{y#TR8XBptinF?VM5vIerf*h19$Ls zj02-UQ`iu2ymP#Ii|PRCUF%W`m}){#k#hk)f{Doj(Mw?k89osI4OH;~`!@eQ0p5w) z`;@-+2!@Sxfljhx8uFoUcVtC7l&!{mlGcW@;IGJ0dSl-nz=~OVycpgaq2BZPeop4l z7H!VH-XI5KJGg@~zDq3riK(xIOlsCy*zX3#t$UvsiWi?6vrgiGP3OSytC)C@N7Rwd z6gvlP?0xzYy^eqDr!m`8SUmB}_Ih^e4GH1-&#lYx+ZUXM38Fc;lH6OJ+cyo6wYJp< zDLJtN#k>=GBzxn!1o13IwUgYYlilTDP+EByR0kiFN)#qXYDdp1?%rA_L+ z_+vnjLOr91rBA`XsAdurGeS2~=|6?P&)69vt`WMZcEX6MYgpPU6qt#{>7>8gTP%{) zB4p}PjSj^6`;MmvQ%h0DvgoGFuz9sSd-m1cE)wDr{|W2*$*ru-;-!n zr%cf>ROT$04K8OEO~Yr@Ps3$3_cg})fk24>smXLv9V!5S;QX@cKn39FvY4a?4(qj& zQOwMlHvAS$qA!@kqxS(I9+fNA=~hc+_cLo%iFlG{w?e-w>S0KSeHMeL0Jv#usHki; zje93Ns7Bu!CdTx=XE0qo8RJPE1Lf8&QBRhq-sG{m*TTNPJU!g%*_Q>|VZA1!sWU@|SYqk6v`DODa#Q?thM)G90_|bfz zl-XEeMdkqhFEK{;ZGeirinKLTm$yvMV&f?|VRT^Fv1eW={op31y4Yip;mBY8^eU8g zsbED+(bMgd-}A3Tx49fNBYaT%`lll6?G}*4xx$yN6QSvbiS{pR1QLRi#6GbUXlw&H zR|l)kbU-WL^087T&H{dQINzMwIja=#3fM=G#HWieo4p>sxLfHj}Y?ceb_2Air0GnTj^%+0lngDXx*Qn{|z+o z)|}KYieJ=MvDfc^#vgjb?y&S_>>IX!HugQc&_tTaOwk$3Yki6v0sS~U@F38$&PI`?&KVkG6g{}JC)_8*uL5IHm9j_clOXm8| zv7+T#!kewD7O={*(zEC~r~NM7mHnX;vC~{;7C*ey->)P_!9vy*oSFZfY zLjjD=u_QLb(=Q?*B($v~pSh#DNpwt+dqh#t;Ly6%;}E5v$9essQpu}Ks`_toFv1lR z;XJRR8*qfoRJbARcGT8lE5}LqmteqDA2BKwrwz6Q6`f*qq>CQs8Qxn7 zsI8|$%v zZ>eOuo%BXDXd|a<4nZ~@Brp}t_Q?HXx)QiS9{-hz{n=%v9QHn`WSxw3vI08V2bFl+ zcajom|5L>(I+qP5c)zY~(ZW#_jtB{K*%8bw-9SmA!`b6_Oyh=LnE5uFjadlqZI*G} zYXhmfs$`Ski1^N4Usn&z?NmZ=8kD0J>eMY7mQ=_`4SM0)>X#xeRB(F<32GXQ&Vkf z;{(*PrCZp%klH!Xc--nVf5KYiv1b^1R`;SZi9uh#f-euE0vRB{q9NqGB8Cq29Hjhn zIh9}-0I+7=v%CK%hHe#&cJB8URLlZ>Mz$*+Airdsl)ZxmnB?}OxfYd`_iJQAZcCeD zZ>n^yF3IMIu8%>+%nR!cZVMhoz5S&Q7xCeJQe!Lr4C)EGPhWb-fuXI{rD`LlbbTs> zo$&Qc?z*BjqELxt(g9sGaPQ(tlycBl0F>Xlfo8VMY$4c=;PS*gv@`@QH0`Waw4CtWt3~5 z|JYpc)UcN_JprX1>(dLmwtPo4Gb~o>(KGmuu;8LO(yOSJ8>H(g-pjf(HLG-4{Fwr@T|zH=~|nJzb* zNJ~M%SgWiiU@+=I_J|L_$OTnv4&DcJIcEaD|f z+mzU|PES9Qp0e!Q5{DTY;Yg4k>t&?7+MoKes8|M8*T=t-P#l*xymfXsW$7TgqY-&R zIh2^tAnc}Iu4J#}vQ?o3pTPdl23TX9)d|zOoy6d>H-)sTBNBDTD_?h)j;Gnc4AoI+ z!D`4r3Xo37*#Q{N6WC1T-GU?;USvM)vUd=*x+a ztTG1z*{Y7Si_>kS9r5|iBG#3+Mk@ma8hq)h;#HuDHSXg4)X(`Y8R%RHf57o-Kf7fo!bzC;uuj)sHGLgN-i>OqoyZs!+{r z-F;CqXVKD90KIpVu6Y+egvgudw|;&_$bHr9MT!ZL5u&!j7(mX;dA|6vWd^dc0C>`L zI(m77!T?LOW~!(Zw!vO&lmc6xh+Q_P_;7JfAjEx%I(rw^JWc2AO#4h_aCyZQ=6wj} zd{)KL1_p7cmbFpAmbSe zFLhki(1j*k&~fq>z*N!5*Zc1CsmcUCG=89KuUAo8H~k6)v6SweD7XjgZo(X)K8-t#MOW8?HAD|-sw|cpbL@$;?#uH z`e1s})GdFGNwymUCghq{8M4N=cFm&avd#=9M>iUOs!WYFE(>cotn5%o4-vHeud~m+ z4jMu-Yizd$Yjk}DS72JKcDk11DNtWQ>}xlFIg6g6v@}IYu4*w!qQ|$X&40Nnlw>hF ze5?oB#VO^NN0Llkv>RDQTLg*OprY^_RF*0%u5}j@0d(hA$6BbaYyn%k=-0?k!nW2rRAvYb1%1`y@uB-X}dwluVHx3djG<2${ z*tPxgD>MqqIA4en-@p2OpuUHDJDw*>G~kd__BNy-?!bazeZ9#ZPVh@k@#~)a?Pv5v zMbb|3>D0yRC%E_-AopMud9t!5k4W==*xUO?R1qz?`5edW(pI(2y~F$~Fa67a|1M9! zNkj7k!oK9-F`KC-MaCecXiGe%F&T}HlUK?T=rVaQI&NeoYK4J_1gMIJ5Yrk#19~oFwPF00>!U49`EM~6UnP=-MGZv&4RKNS)5#TFb+08YU z++4Z+;DPkp3tdx+S5*pbQcp8)(+&y$x~sn~{a-ut=ijEB;gaGbp>Nt^>(6G%UjE0w z{=b&#s(gy1RTVS*LfVX-cqR{a1)B99M{CFO0jAI2Ht6pQ3?hqCdIiDznG(YX?-q|n zo`(rCHAHV6t={;(4Wdy9#mWVfeIMm@d`IAKJr6@flPx~vVET{n`xmbEzx`2Ug5;x5 zM0ARUR$fLeIqnu4AZ@3+J>P}sUj{w@u#5ltmXrXJk2px;;d2(MkDa?#Az975zxPQc zYYcJz=|@*ta-Tu!B{f5qZ6(Z#=A&`42!7{h|G@Ca`L<{kf=kVw3MtY{+V)pZzy7;R zCXOQ}+DV?6Uw-g09)8Oqk14^GJK2TIv)T9;SM%TY>R(SbNc53t8nW+{Rl8A~riK!) zb*cR@{+ka@m@S1Ba20*u4Ut?ogiy{@Rq{c*{2CAc<1gp(dVZXPKFE#kA5ng%kq8|N~4&WECIy(byy4C* zF%${V*}c#Qg>c_Tn&zr$r@7qBa#X)-oia3WE%nvyMPyx@PpVxQSbuksiNG_X-={(A zOvcI;^WiPO|F-urM$#YT7eht0qQ@=p2d2{_)PB7w|7mldV1$LDaD5MSVj-de06F?c z+x?eE0okUcF@4ru1v}Y;p2HL6cMc6=^3$!`wTh0n{w3u zeO!qM1D_mq+89tblECl}R<W)s2hK0n6g79Y8de;4@p zm+AeRG3($eg*a8I+TwSt&%_JfFU9sf8poXYJvA(VlImkz2A|we!n$#)w`20#TZQ(& zedw;pe|4_bP=v6XqD;`laMu~S4X6X!ulVnb`PS{KNd-o49$wxe77y{}r7Bw39qD$g%?-$(ipYPl&M8#e%VtOIC6x#udOUM^J zaAo&jPwYQ_T0!h99+F5~keIiD$6WV|Bs5pe#NM(mt>m!>Vunu+J;Oe+MCK&zQCnv%f$tWhr4v+qt)UKm{B*w$^ zyX&Py4+Qxxcgv|PI$~5hN{L=1f#r97H_;WwYEucB)gu$AUNq^pmjBJH-*tQnJh)<4 z(IzT-(cQ>8gz8kXoGrQ;uB9R?nZWbAKcDoTWIC~*Ele=2RASw!NALot@^^zILJm(# zWZI9jDLS=$yzQ3mQ~J8!U2DRRV5hvlOJHTF1zZ4V)KLVZv4tXp&`8K2`J6Ymu)6Il}*D+Cu+UA_1y_g{KvOwon*x0`!B7qJ3gsEO1;7pQ-}Pw(H7i-ru5XL9 zL@jn?f}+;If|k>dt7SmP=zrGoKg}RThu8|ZGmQ|ekdXYTK-rA zO*dvB4mJj#<32w*xYW$!G0iV|na0c?T~O&k0_a!wozFn!hXi7HxjO$A>s-E{6Na2s zh~$R2`0HuR%%1ABuQQ$XciAgle%u=ntU|l(&Rceq0C^N3{nDQ>2v@ZTR(hXK<+`f9 z2k7bXY<|=hce%5vPd={n#9L;L@6=w(QSdjMTBlYOT}yV-33_WL=zMdlzlqy?@&UV> zQshf=P$Uhy$JaZY|8{799cB}odstEW%hs{*N@Pb%6 zWg9>q6mDb900WEhtOKp#nV_HXv3>^P2=G6Gd`xg>0ETNRW6NdO8)UH!VA*9rDYTgt zA7~C$d2k36mr8~UB3n%>h7%c$-vJ4-mn-DufIreZTnKrl_RX2{iY!~KpiAXmfQmCl za@sqfP{3`!G0Fpk5*>>ILUm}JOaP6jR?);8s8J1Y8xMZFPWhyNe|3OPE#r0mrD`DH zYklfCvONbfcd_Z8o_3mnj=@U-MBal9KaZb~hhJ@o|I5-Z28(=c5ghchHH&VxPli%FB_Jc@*=L zd+`1|o&DLJ8E>%;1!tWz@hR21z*;Hw+jTl*!ztBD$ykLc`Ki_6*Wt*m8BmFzb_Mch znHdMGU4C;H-@UGW(gkN1#ZVnW!Piu0ULSCG96JgJX*`RHAlvWP8gr{RuY5?}bg2nt zeeMY8sWVt;IFjl|Gu&Z)n5ao`Y2>RMPQ<+{xHh|9r{+}%13BSyyGwnx51gS{NzR?V zDk_CXcA2~7CL^8KZ*zRTewUjGVt+_Zbo;T652(MrydDDN;O&r&+gc`gI}xL(tkW0_v>r~hR?lpbBC zaFVM>Y2gGu;YI3C?iVIl{8I8{Z46yU%9Fdy=1aIW zaylF&-*CVNj{&6~15M%RfD_HSqd{qdM=UX~#Mle!&rmD|$Z41X=sB+(a|-n7S+fEN6_zQ?syDHDjH;0s0XX;xeCrmi}TaK>L+Hg$y@h;lqw^Qt)Co1y@kJP@?r{LfL?Id@ZqirR_J-|tcNqylLXU&V(0}2=h4hZ`(rI9W- z;0(_V7d!5i0}5(fu1-R!fvd(xmFc?^o9 z&(v>e4t#43mlxtc5!?ft8@CW5JR|V)Y_G{AwLp4KHMwSTy2jR4uzdOxXb*+Dm->zB z$NfP&1qzmk+F1wRv%mX8ph_%fHOAi+kBBFDOwGD?%6!V1IF9V29uO3r2hYz5$kdKW zJlc3jXEg^+G)b^PByiJ=D|epck0P?w~`OUk(0yQecB)RPTrrxq+4x1xgT z2HYreqD-_7+X8eHX_e2FPNBsF*~pre1|?;w%_)zc4sY?1s3W3q8Cenrl(FqfkL`Bg z2~Zp><2vk~{rTQIsF{qB)7Nw48$PZ8e7iD_|7@o>6BLL1bK?*Wvt3Nsh7uH4e*pah zX*EX0RDsMhAA$-G3^o3W{@ z$aICt@M8B7Wz{?E#cd&7(WlOnMNHRK zp)h?{<^K{pMkav_$}s@0JV~bmCyUH2sKodTQNx11FWThhB%2b%_D6y-i#F_9t(=q? zo!)FEkoy;p_-0q@rot=gK*c^Dc)98^xo2B?g|#PlZTB8kePF0|k&g%wI(srf$jP?t zAf}rBxOEEf(iUuHp{~2WF9~jX1Hqa2YkXzu)KB--(}jlKa?{c%uH-K^`mR6XP4 zUj;-34p85=IZn1++Xwwg+?I7j{zB;saJ#j6R2ieGwC-m@<}hfb&5{&(=ah>y!?RB| zIC`RU;7Iazno_e@vTTJi#BWwkSy3;czr_<&%J8Xj7}wkc*;!+gbodXg+GRr zR~tmrPA^5>@`}5?-jny&rzCw(l}k}|6GNM2E0X_b$&JjJs4qIb>q0zdI#Y{~@1=CIOjUDPxM}2oR}!-xb{8v^#Hv38Fw-9XKBFDpY%5 z1A*Aa@i*C>S`d~)a13yjh#!=dkVBnl)gdHdAcUqu$W;!4HdR}|mw-=q=BD?L2hfC@ z-G5W+z~m<)aAiq1Bsn3A3A3@AJIyLr~Jy64a$6 zr*qkj%PL9{Pbrgk5qQe-PJDBweZ*l|cbqu^8s<0we|nVuTjw+5^kCl3qiQ{}Ea77% zyJ!LIEZKOf@VpDq9L@(qppelHeeroebm!oaw->zbk7(AncZW7PH;%Q!WP-S@pA#r9 z)&GnnQw9R2$&qsnyR8-ZjAKZ8ISu1$r1P5FT@xz_hVJJFszdJT_-6?pGjX+H!egA1e zmqYGLLBJ)zInN)E{!vbL%?XW=cJF&hfs=n^b-+Bh_;AUx_uGTl^=CGEqKT*)mh)GY zIvxz?3RxyZANx!T%c!{wWN=FaCB>759H*`Q6wCcmfWndaRWP2WP5sW;*jvMUF0+kU zBqsLm#90S|1Eu<15m~38&jT*7-S8R&l4gr}mF`@_84ZkX<)1X~s>Ga%l1;Y|PI=2b zn5L`6_AmY$nh_Zb@Z0plEu=u=G%{g2?@oMVHD@yXSryQf)o7L$WU7@mQyq7T9i^{j zB$*=n427bUJ05)Ld=R&$t;*qOYW!q>%{I43`1DiFR^_v9>c*ei;BGVC>$gZMe+A=} zjT88;>4-Qe?0s1868ZE&Hr63f`knAythbu`!*}()VnlEeAu;l7YXKR9H5>7c+7!S2>f{rKyAVOf%W?8q}x+@!Vnbl*}uMXjA zH(iV4v!t5l23g=uFPH0KR($&xCZgfUS9Xc{fF@;dx>Xg@@Yz>Rt3=0FdV_>ASWMJo zn}n61&~z~VbrY8Cm;zwud5b^Zs2y_#)R-Qi@xAb=0A!AKu0T-J7X9uq_BZqP7Z4J1 z@63fuU95MC&Gs2jFPk|6%jZ~_FhUrfXYQ4%=-rp4iMV|R@|?TDaO0s&B!~qc&1yDh zOpcX29};^btT@ay09!kMx}Ke$rW`4?0<>m=ELalGfE|(}4p~luPWq}k_oMa*CnFl>UZ= zd3FL2hG(OV&C=o3Mqc8ih3YgXO{FQEIIeFZNm3+wm5RMp+KB*f=lO%@MMt?0YP;|e z@dEWEOMQo@Wp9zQfQAZqTQT_8G>wpOk1s9{NnI|rWYiB+Q^mZ6H!*)K9X{V0J4{XU zF~%igZUk&4Ac5=9l?f-d9~sSvhWzmPdXBa?*QSNl{wNQR!49D!{s#rRfOC__*Mci1Aft){XAPAUGeeg|*V z6!w5L*UraiG^`eTt>Z*exo~<7XaPDX*E$GJi?dW8PNpJLw}yap)>B73OE0PTdbEuf z{OKF1E3!e^L4dr9X%2a&nhO?pQ;$%@)dqD?Z? zZRq7aDSdK?U0BUt?=#(8f#bz+Hm^&yt$urkRExvS2@90knY(#cxDy}>S`bU7oim5e zQBh`fy2eozWN{)+nby5tBd}z{Nh+spy51?;K}cl#fCM?kTSN8Nl>xU?Up*r3B8ffX zH6lEPYvkMm{ql1HU=;a13R}4o*Av`7@cg!g(@iwO8N8Znx8-#&83Qf^r5>y=-8_aG&LLnV8qAsQ9 zO(?xIc&GP$w^DbezfT5H(!=0+kSB3q2PcZ0*zz7V5r<_2-Xc2|kPbowZcU~pY!P=a zy!htQcR1JN-|3}us<*MkmdBh17tpKgiKkAQE97-VR8(}exk>u9m}O5FCGz!x)XUA0 zKB^P*+1|vRj9Z_U_`mEdcH*;L(cMWg)zspfwyHl~o}yQAO4fa9k2Lu(Ypo%3geJ!9 zOqrW;*{(`@n;hpbYO2@haH`%Hme*aga=;}omuzA(l50%eKFY_3c*2@V?|%`CheR=q zu_TTZH73F=mbun)_3J&FeO_7I_p!;YwDd@E5t*zsr(kRjxx>Y))FjBn0sH?Ld+&Iv z-~WF+>Le5mBMEJr>}(p85i(B3IVuj>dmdYn%(BVO=8(-XQ}*7+u}Vfb=g2zt`CVS` z&v?Ij_x*idf7C5*tSFEyT;!NZI_=QNMq$zPn5 zN?IIT7e$ujGoZQ{Q0-XmZY>k;zXlKUq-qwEplgJ9wW-Zs>b?Mw0JAs%=cf+yI~&K3 zpi&?w5TTXXP7UUorQjjxP%Z-aBX9lL*4)n+APKjFXPj52dk-hq?sISkm=XWgihNbK z_K_k(*`;nk{{7$xdq7maOr5Z1w$OY7KL4W5eKT*>4}HHMWSg|q#s)oYJFfQ=0&++g z*q^!+OoxYwMN+R!F{h}eq7>xJWZzTv=ZSglIvP&$iU?>-waZ7kPGqaGIe;HU5qtsF z@T-OS@kh-uA~5S=-*6uNLKf7cq%os3Q}#MlE28oyoIpm(V&)>Wqxj{Hm#w^{k0j@j}+%| zGgRb#Tcnd0r@4NKaS-#1yDVhyMVy8ffelq5!=@~h>^jEzYn?$7b+YG{?r8fn;JVQN ztVZzAfB3ZDX!=#YA*S@lNfJ%kOo! zTY-#?=2^rSs%_!Hzp;QIg%*6fKi6m46=w!aoDy3jHaxo%hOIwX+4xITyqW_}j&t2D zwcN}42$kW0@ne-8?=48>7NeNPyAKk-Lc61{N`D%Bg!i@3C@qVI$!x~}TZ}$~*sI%Z ztIzwNY*AgxAKGm69ZejCr@W|fmh6_fBQbEg7C-*7PiX!-bzFb*@*dv1G5Q2>la$GN z-96Foy1>OfUDq0KvfX^|LJTGEOtJN*Kx8BA6#V%$04m#seV z!U{${&D!yeq0A5~+{>`c>CHYp+AX2}eGCRonBut*7$V+M92l5rWnU4bJKybLqTFGf zn`KS|Pn1T6iQ}titeu+)%qvf`Q{3|hyhf{j^6KNGZ5xL3N5||3i`WgIaj%V9LIB%) zOLl#az-+bp-O>S*8C%tyBh*qyoIo7^H9Dq5?-Ku@;zCoZ{P7IZNxFH16@NM1^Xj&C zGmXA{aXF`*>gew9D`{ro-kiH`pnxL3b_B$BTB^~9OXj6ELk8=zQ?_Q7d?U8(*^84~@_leow7xobg zBfx&L6l&>btze6tHOOjRFtmF_fLu{M~O%@g$3)+n*$Pl0vGPme$sLau0X>m zJI?BJS5DN{oF~eW7HWfbD#l<=z43$dza& z)cL+#r7yIsQe$1jGx}-kIUikaCA8!>5U1J*=1sGlSl6YYeJZ|=$J!C18k#!xW>y_f z8+Mn*QgBpKT#bdjiVeLvTDu_F!r;MiZxR1Tk++y2Q6e-Iqu}wHSIXWKYK<2Ez%zq# zYRXEjYp4VLyj_!X=z3wu{t-bv)E|{%2U`7)owi#9$!Co|FLmum_Nqi2c65<-zsWZG z>L>^m9Rmh11oEw>gU>z7PbC6L+vAnMDF|gyUy?WMtA2A|gte{#WI(lQQ$N&!PhmxM zizoF{a)CE-bg)=UH5b_;p%tjZ862n%q&4PF7v0OsJMEgp7?s9L>n#bS7%%FXB@}NZ zNYLe8Z$CFCR*77xD9=r$a@F+p z($i!o&dMG8-@kkZuFWWQAa!vyFCik6wcgLosuRgkLK-G$@7VjZrF%kSA~@O_q)`oR z^*Sr*dHNtidJ$#6XwC@Ruz`Nx&y2k__K7VJ6FB9fc;>w7P+oZ{lgJuok;|sb0Wrv7 zd&zQYEGmEH;vg3mf=*>=w8% znU2dIeHL5aro$^XkekMdUwNSs8@?25g4Y$p>@!Dz?nQ7gLy@@X?EY+fMMy0^(YpR? zSia;4gq={c2NzmNqksh+ECysg`@UoMZpArzQ>rbjc}Zk4LvV`TlFf##zB9EL^Kebe z?EsChZ!IvW?nt`J)ZgyMIVJMS6WZ!yn(QUGGS7FG)qVD^<)YK6>czVUNqcQh`>rEi zv3%84m6CetGur8h*LR%KTrgF38Sy5P3o5Q=>G3-~l6Yhh#dhVCeQ9@(wCFcGNoT34 zoa;T>P@+gjy%sy26UU;nIaKnrJiBJ0-teovPiM(aRD?7FIC9P}m?||dx2x1IoGQv) zUYwvbQlL947+Kz9!KKW4c^Rt@jUzJf>RqfmFLLlWTEyyHkp8xwm~jm4NG;UJ2;g=h;8Gh&WoCotbspRSabqwY92il~SmCGW zuZ-TAamN8LWj;ix$Cm$0P{iXwI}x4QhE4n=7UDZB-(|6DeT0PwAb!xdjT}B#jz|Fs zMm;bx%Y(y>(t}AcP1TfKi4!MCC=cuW4UT*J*|pQzPBMr{iz+f3*ORbtxH*>^+vzHc z2}f-u4%97E$mr&p*cpvfl9zh+DNlu6q5_L@?I;Ljf|=4dEpk{6F06uXjjrm*l;EdC zt}C&C8y{V8WB|s?vOdFh{;3Q zaXOFb%Sqgm7ap9cSjJz7PU0_;cR{@DEx}k)c{r8Oej2YZGlF~V&Zs zy``?T&4d|Al&Wz`YMX(S9`{q;-#R3owrBC%%sa($YCh49IM12GFYz{qq0GWLHYT;w z(i{G+kgyArt);zEW};T(bc}CC?+rTRB&#U=-E9G4NfOF-US4WAAyz2YP%?$BlpLS1 zTbMLKTUD0F>lV&dD3{44(QNo&kY;e&>zivOLtdYnC_?WQ+4YQ$$Fe?N8rRt``~1Pt z-qhjY8v(ToN!SJz+5(;SRc^Wj8=Y*`aZn#DcUd0)YQnq~y0>>u z$}sX4I-b$cb;<-LW6kPm`D$k%hI>RAW95(HPd+_8BXT=j?$NnzK)0}(i&U!^pW9f$ z2=O6dO#L;t5Q%x5iztCo`Z28?0+)8)#ZV0DSu*(xxaO+DY#*c3sfz^HTE!Duc%l`rO$!_j`s7YuC?pw*FAZz&C+Ulj{g#FH>Z)5=-_ zSnp7(eKw~djl;?G!H$>E#?k5Xu#8H$QRedHCF9g-Pd`;MO(&T?4G6zO%`bmST@8U1I;T7)20?Gy>S8=53@H(EQi3#IdSdj18Hc7f9d)E-TMFf zmv14@jNpW)8}41F#?Yh!7Uz)vumydMAbeu&1l?*)mW{WV*fTkA=y#a!f>(vgj#VY? zC#Ftl9Ak^DDX7uH%eovxP@)%Oe=>+ZEnMGaqXCDl@%rmC40IJq_Vzz~D=HgG+(laI zS}sAEaQBD|>YWlc(LkGwS7G{(vjC)O)IF0WHU3BaEUosTiLb4_o#y+C>7ncwo5eQBFlT+A zaHY!W+C{&oy$AA&Vyk@6d1wyTbi4ApPZ;{kiIOfTbfkfUw9ycWQ(aXsouk}egpP_AlAQ$ie7BgJw2C1wNJ>;vAX zkKs)(5my7PKwINHnr^X{AYpUELZV)}F;ce0}?3%MX)hx35PkRa9_IGi{|Lxyx4Ejlj7 zW=IUsxkNT7tuZP_N*B7G*l=kBM@Jyze{^tj+E@6SrTI%IPmYvEvE&~^!tLL@ARy9` zCd8j8^>MGW*0M*xrj3b$?znnC+L;&iS>fj_GJ1KLXnb?7{O|@6PMApG$Eh;itmCim zg80%(EXpy;ZQ+0VQTJ3bJ$@%Rx?pJgj+dF_Xz)|f278~-apQ5c>W7yp-wn1#utG0d z64ZzWh5F@hJ1qgWf5_QdFnsTj9>isaI3SX2Y)ThhP`b?sa<0xtWNms|2Nxx2+%rzx zp!4Y!s;%-+%*e^5Xzf~XMk!3;#*#WDv?6NshRyLX`PU#D6id-mE~JyE zZ)B6td1ZZbbh--KXEWLo3Tr&rDzqo81dnf|lGZwl9H*%v`R;~}&o?4Qbt7r59(f{$4V^Yc=9?(+LeC{l@6(n* zZu!lYWh0ehhi&rZZ{(eNhyOIjN_Ik!y>IML5+zrpRFQwrhddoFLQm4wI;n>lxpYB7 zlceyIcn&g9`<724h5S$g>G+8etHAr;pVPnNqL};I1qxu zoa;$7T1_vMPfSF+6?rpL*{>NUW*wRO41!LSxR)h}*&HC010R7{15mIqPlURq$jRTF z^Gs|};R0lUmqfKz%f&>_m1>W`XaBKro#&2-{g%w!Zaj(@KYgwUReK9Z>Yf)o??fb@ zeoa{uoK;JKJz`WiXb_YQ6rwtcxT~^5t`(W>UX>hiEn4TZ^(9`AOtQpZv`4cP?YY`S z-mT+CgP!7v2OjDW+L5+QmdpAcnpfuUS*$ZDZERu5mQ!xwU51$qyBeRnl|nKw?^z8k zV%@4b*rmNq&A_8l<(3|pOI4D;zZ=Z2T0G}wT0JA5o}|6CVMtnfS>q^_sM)yt+>AZDLa>S9!E<%r&$0=>q5>^XD2Sxd$tKlxq1 z3jUyFCt3h12n1U>Z-VV@QrXB6i5XvVJe!qUP%Arwn)L!h0PC@1xT33$St>z=8(_9c zyd5YC!0(dgb9I;NWMJ7|3!3ayB2Hc$DuaX!F0-Q8jxmdU$2z~84RuCf*3bxvc4BL? zTkt1yak{H+`dLerY|5`>MibXiJ|bc^a05S-XI_4Dyux|=Bk1L)J+~5cR$+v9k299; zTyo>2o-&mF*00DW0xV6`75Fvoxh^78tf^xvWsioYuirR^XQa`E0Zx~r3pO3gPE`>( zS^q$m{PAziy*fF>{@Uor)LhT1Op>Di@;kRe;#aNfiLu{hH6fGHO(+43Kkvnt#1RX9 zT*jwK|H1-9#d6x>gF1eZhH;-tR$-Zgp$iH(2mGy&iSh^AZ(1!G_HWJu(|?gzxN~*< z{?f-P6~c^H&^&z=%k3G7b&$$%<8ruARd0kOUib{NXz|{L7)v<+u0Izdg1zG!ZL-*% zqOfDn6H742EveDy_)!@yq3H@Mea&W*MGoo%wZx=Klg&#RpFU4}5h z=)cTtIh`cvr1!3;!eUk-O_FARwfQ`h3r@QiohD+kQOc*!?MmieENlK_FsqfxH2#Pi z`(a>CYkf=KCJP4l8Y})b4^oCHS*N;5>%Cd8I(6S1BiC1$X}caS>>j<5btT?!#hu<| zP+a(QNXfWRKiz~TlLO*wg(XJtLf`~fYu}xP4wOznrQf%o!u)->X+>Yz1Hk4IhDI37 z#^rN9Tr$i?Kp+#^(sHv-M}XtFl%ukvCj?huiHuGdRTj-q2raoskivPTY!dR{Qd0GC zkLdv)p~bvsjdflJyZufGt84vrR=GZCo@U3?UoIP}^CaN;U)6i3e-|V$+|avRsxl2B z*-{H|55{RY`Ft+htXLY1vE-s80po%|8H9!(?r?2AO|329_eLQKSRnYI>&9#E&+#yt zRtkxBuzD8LpZOg79?dl)cmXnbAv&0U=C-v{@k%X2O!+f3{!#qR4v+z9EXJ(vEsV(B zR*L2tFTkDy1WJNHG?xySRxFwRz|QUCQ{6mCiVc!ECjrq{~lW zgYl+Yr&w$ai*0=iqlz03l9;8_vs2R>k}q|wg19uI zT7LCvw?6|0`G`#;E)8O4L#J^?Y)<&AVEKOoP(T$u_LiJ)2K5x(X&HX2ocnV!BVx2e zaBN?mXbhmEUe+#GwX!ZYNB-a@XU~0ipWQFB;h2IVhP0Nhk!Hf-!7jsYqR{tGt+3*{ zNZxR-1s0M6?SMbUV6#*7r6R+IWHW45=uRceJ-AUIaapWUN_gctpk>w#P)VH(Ck-fH z=$<4_FdKaMxs&4ORMP!q^0~U9$D?gGQiy3`%IeRQu^8jx}?*2Pj9aLmcYzYtbztI*3aL z`f0ITrezkRJ&=rGcPv+Cf&C2J;wASNI<0^yYy}4aaFBUA@Cb9m=rIE5L{*Amn8pru zsF?-Ao!dd5<=l`v5sz!(rS(r;8$a~fV^8t;?lrRPT2#Ml-gR|U2=pLd$uJJs|44Y1lJSP z=t78WrDbYt9=0Z}4^`%Vz2m)JG_t-Tbm1g65{E6cU_A01O$0VUH3eo6?%?rpQ0;zj zQwT4k9jtN6kJ{tW+pp6vqB#zMU=Jw9n`w+vD_-`$Aj01ts++lrK=i_-vpjucvu45t zzX41805`MG_a!gOHN**YP)jv3OvuUmmi9^rePiksC)O^m71bv!ldtFLsSi1SrX(pX zpM^(UD@=N8Hyd=pP*jYz{^qQyy5a{4Jb!)9ZKj52G;ibtsB(Ukb_9k4i%OcNH9Ie- z^s@^?4Z~Dz&OZmfI=`3rsvS=GNq^!N)rjNL5NFLkz;Ya$_7}--g>-09_?PcuifHcV zjsp%WOQoK+qMsFTWO>Qs^A zj`&m>sngT%qa#OUj$C=YH5pfRvV*{*)5=3M@sl0&8_mv>kjRo%hRNm=>cl2`1CN4V z0OW*mwc1&m+j40W>3>TB2EoCURmhW5TZo_1{dK@1am0LS?At+-27AzP+S5eIy7~)E zgajAbBYId8Z|%-~!lk?y%8nP69Px#pRLC|QONKUy^~Hr8O}n3yBM<%t#UAY>Psl}Q zk`Zjh56G9PY8I?}IxQ=qXF2(cf_R<8^$(?T{ctsxAY=57#H>w}0G2+2_T;2LX4RVg zkRz&*jAF$mOC{bs+08ks)0Qd`mu5dtRbjEGQ}~3Uwi*7k`Q(}RCOm=C3h6i+iPd9b zhXo}Q)1^kKtnO`BgG7UMx4>XB?vqi9LuZ9C#hbX`hl1CMW`k3<7GNFkWCTt<*JDhi zEb>9K=GFh2cXZgdWBI(IbFBg>KcYbww%pYfPpGor221_1(@}PB#i4uicu^@P0H*ZC z$+|Fj%ro3X)Zr7dMQyrfc#28(7RCKkVX6=&p%xtp@!m6^p2RJWsta6F)8`KjLVc=s z0T1)%%Xwc`)>oTp6D?5Hx7?+vPH`uCTU3GuqW6f0E)Z)6%Fsm9WVc%;y0#`DXHy`U z?zC2gdvR{*>jut;$vk8DX}+$q9=F|}*{vr^w3Xn~Y~Gr3jsWc3-_LdF+7W`6Ja}_DGh9!+_sv=1Gm1BxaWb9t4Pvgss5Epl2_I{UYu^G z5+d<+WyTILkL{AshkFipL~lL+X=<{oZxbIb^H$2XzKZxjRPv@lW2G(h=|){1w8*-% zHrXoKW6f;OEBPm8ooDdZ_H3x&lD{sDN&%osgCUni#zUb+cB1_IL`WL{cV&H=&3IGA zfT<~LaY=VQwdN%__9%dRe{_no20>l05G1^Z0LD-FFRzcjh(XMW>hHWqI=!=ipgfe9;@^zgIe4NjxV zZrqjP2Pd?skbv7Fu<(STq=c4!NZlmQt7kEjM!jPkR_9)h{JD_l$4$WYz~RnCupze&Rq3^dbzMY1#s)BXxYC<;jmV6{=*<7Vs>-?>irAA>)0x9~GN*teorOcA~`n z6QwrZ#j=rpbB)-sG-pAK*4AW-xQx}fFcG$va$Q?|=ULz}Tx^hq)`I^tlZujgHR(+) zQs&L1k=v&avzEvX3-I@on3KH^-HyzW_FRJFM+o=wE`GB)%5n!aZIQP|P@)%LsC-AY z+nlSk=jH+I7=Mjp6BZy@gm?=K>PXh+IW$FXy2L3DUDqRa!99wJ@W7^@n~?`&({{6_zf7F&jU^{6QBzV6!d@FwRV$d(0m#Durb zSUJSdRQZS33s%;{Hv;bTeL!ergJS8Mc3g4wAkz;plx{zHz}~RszRlvHAI2m-0O|7# zesRm@qGO%L?QcM0Whbu(G1nF>2f12X+TD<1`WwHvdH7))INJ7OZvwPyD*@N!OH8jOvqZ|K}n3(6fQJk0#oN-9+DYclK3j;_5+{{qjSYVVSKTtA*UMW%DaAf~r>sJjZVz4ayFx1dW*t$GH&_!gb zvgBR#0lIt~@VG0>AG5f?y{|}KTnxkB`AWXCyih0mH2X@KUtl|)7^813kY_n-b7@p+ zY`@{U>oNmBd_}dT&O&7kWG%Jus+B(jbP$*LO*lhYOI0!u-#9WN5|0=d{W~-F(=AQ| zFH(Y#`^KXBY$bPF(J^!b449nHzqua9ak~?S`IWCzxJFpYyS~$jW3!RGo7eo7w9Do? zL*~%`fU%nEfq>*fcTV%0hOW9pzFfaABbK{UxsmsaJN#T!!IVCY`Tgg_Q6xBjV9}f; zK~)<9veVr+bC z(OI$jnPZNB;8>0hPVWJqGTW`r!xCkFfrXUOPL=YxvFU`v7~SVWTbsw8`qD~oSdD)S z>VLYRnkXWYgc}>?)vq{ZxA{)h-BGstvc2uMPB_EOs3r_<MV{~HGaBVtRI-*~Sx)C&QR&6l0L}}`g_^xO)eJ&VN!zB-qiCBEY@RG! z^S16>6}H@Bf!Cg}Ye|0}sK-)_Vl3vt+51L+-Z0f+PttvRH1}0qgJz(2Qq~10u8V@k zZ&=(j^(VY`AA>Ds=hoAtEB%{h4__boe)GU;gx!^*UyW|QbOXs+-zCx(s{paUL4JMl zvD*c3IN5fkpLBkD^^C?$0|6+1;z68J!n^B=Ehns;CV)M7=ivk!v%_hp`==y@pLB&W zeb8t!I%b z-yxq(s<}CeXID6{Z|JO?)W5)4lZ#K4tu4vWL5MvV4CVUHP%A z9!a+0=Z8Om9VAgLK3^VnXn2TJM7%QNS>cst-7?E|7IL==%A$y3cf$XG$4jLMemjq7 zMyy?)n|lg1bjqYICYW(kziNN`Bo&>((g`mDV#auzmE-QxwnV={{f(okIcyViv}z>$%s9v(n7XWE`Spxcr}Lw#m9m#7^578#oTpOE zyYTo)bnA)agVskw=NcQ}4Z$mbuqnVax0{@+*LxI7bV~f$*$vdMBhjU(|8rfm@r-v75RHE*XdB#qcG&}s9ey`P?(vMGh zToE4j=-%)5RO-~xJ$)Kz~!acMk{53U)Z;VT2Fp~CL#j!FcSxSPc@_AO-D_4zYZP=x{i z)jfRlN&%RMh&BhDQLY#-92D4>H{*}g$4oreE0L?LEb2(F1diK$IR$pT^kZtnbSqvU z5q5$&WC)C%X@MXk*?e?S0^;PohZ8FWgK>pev%9+c{I#ooohAUyk~JF1Evtt^L|8Og z7=}2nj19Fv-dfU{p;gJhQ7?A+o-JE9_wB;XpQp*97VpFxWDyc192cVt%&IP*(Ri2V zpDN*f!oTp+vcd4Bp@gv|m9hc$v$qyer(Nnk3q!C@D*th6&&gkr?CdmhdAIp+Tl!W9 zYR)~nG%FQNzTBixEY0y*>c6&O%ST6SU!i%&jt;Wh(TV>O?+&QuqU|hE^r&&<92#~&`9q`y z=%|kn0gL8|&IyU5$jb=5Ve-$yo;qB--k{hH#MF*e_7lH@;9fDRtZZ}bJS{sG+Vhw_ zqGmjZomtvuj7~Ju4pv9JRjH#|S}e(YPJacOeK9(Y-*TOtcmhk!-RAWh=$5I`Xi~-9 zW~~p`LB<@=T2|iZG^xZ=O6ptd-yTeQhO9o|vYq|c_e5PJg9t>ycBe^)qYwqU2=LNX zCXU|u<}PAB)Rto`^Kwf1zr6q)LD^!El~(~G+(R`O@;cz4ylsZq9WXUfrE=aAk(5L~ zBqnfh=tmvrTFpT6s(_|slB!gHz*Fn3!CYDJ0AKCuUB)+nUz-SvFfa z1J-KOFy(^w-NPeg|8WW$$D4g3AA%Zy?dE8xq8EsJsT#l^KY9Ai&yT-=nz>q_?O^Xj zHoY7T9;zSUoDB-i~%xE3?W}iVkV9h}D+ASyp%8WVN)N`fI|;5@G;!&HOgyNGPYXCkgnj zTWCe6eiVj`J54Ta*#69nt?8l6^O+HWjEj`{uILBkv}L?KcGSA;@^wm$Q7vhmo2XCS zIcfxlY-J!U>{W-rmy-teya@{>jhK`i6)+0Ts!?V02Z_uD#t};ro28FP@a3f60Cyib zw+n4cSR3_^B^lIZ*k^n52BqRew!LH4r5MX>2VC<-=?J&QG-PL@>V_A)iu+epB-!>N z)vbT^$s8Hv>x>=*e1UGH0%C(knPkP8;|dg5Eg>-27wH#D|zSN`m6UZjX4m6 zILJSbK4}9SR)eUHE$fM7Rr!t7JVq=oOJ3w*Bs63@W^c0H*{uZ@3wU^f4KVyh0`!V| zviYkOzY&ZCdfNzO06a3qVL?PPQ&-QELokxnU)k;y4`l$fYNANbw; zrpOGw!B9J>0HK`mWgDn&OogDf*E^%FO0*8bLpi8iumvq8} zCX`nfS~IWyMLkyMAqU7DwZ$h^M-Wb8GT#-?7paXY0HwD5V28Z?j)csg<_Ij+^D{y=fBPc!^z`M_%!&hNzNGtA3X zzbl%h4K}DNdfq0`Df_qI|LD4L>=San8l|KiBjRD|aQ{r_(mP;B-_}CAIeBDf*b~j~;@uG5_GPi_#>E z{l;2&od6kGpK`E@cY_)y7ZB}tH)kr&{r!>n(}ldJG{)X=9;Gv=ByCH9zV!Dru`YwIbu+j{s=nxN+G0n;V) zyRQrE3Hbkg8UN=Y{r8(BSIy9-?<<3~3&+JevKfg|G|-}B$EQ|^kSy0VPg7wQsh zBRY0BjQ;PZ!~bJ-??bNLoXaTvbgyFJ{Uko}`S0A4KV7k}hp(j}A6h@}{5rf->BAwu zaGvE)U-#SReC@fP2~BJ^+_+x8=SP^+dBgmVc1K@1=a?tsp1w|yX{+ZuZ7J`>#Jbu3 z(VG~-ACZ)wTEkz=8dSI3TYGN8b9v3hi0Ze?`qPKLYrGMKSYkLfx%auEEp%cs_8_?- zjPsw|_Kr$9=Nqw;fir^R*UfOc#+7@|;_m-`lmGBDuNYp@7ubwztl`gqEngFGxklJA zR{1CU)iaffj&}6GFG}GuzVqJ12j4DOwqE;3SDGkS@nmea`R6a?4(8Lu;Qz-H^0)Vo z%h&P&_5d==c{Sy2u+u-e3-?c4W5Uj7WzUp(l3xwtB@npLLhF_PVHwn=rot>Slk)iPEgTMc8RmH#G z-lOYRkAWCG+L__Nzx%QEaHh`V)nww1z<`bF&d1DAw zjNBJR#J>W59;<-v#Eb|JwBprUK{x}UYH=d_gmshOYm<=`jDq~e?&fm*!FYuTurd<_ z?I`Rsu9?rE)TD4{rkS?e8Eb7ZUM?xIVr`8EdbB>k7#UbGD$1~%ZL!-s1nlYgQA21e zuUeU0{2YK!l*+i~N=T*JOd}{*tUa?00yK{*oiV(%x>NtxVS42rdBsN$>-Ro2)a8ng zGal(xGd=}Pw0PYfQyL50-L4j>#7EL9i8k9uQ$=R${URo5_X6)vi8>K`KeLl>gK5AU zeMaY|^asIRz}?2ge5AlkX6H^F&zd5uXBW^O4xVQ9(ERMP=YNk)$#pUag_~ylV1q(i z%cW~`#-6F46I}=D)$EBD{oH4JosqX(OaITJRW9I!y}h=q%&phbvNg$uk7c~M?k&vS z&QzDfEXBg@?R=q!UqDz5h^J?$f|Z=1vm|)_%2b$*gBHQNKe)9p&8Pa)3R5%k3G&wo zHz6=CKi%_y*2N6OXdrJ$cIoJQt;$oxfB0baB$<>EE+g9Ugx^xKJxsMhZFwk(sXj?| zS|xEy;kjDVe?c2xPW>34W(0#$A!wZm@C6Cig$|eiAWyOM#aF7qkB=^Bg1hnF*^RTP z)dBVNQ@R<5?Ch`_)UAHjL|zKOE&a``OyFBH)R6tr!GKheiJA0fQ~6^B6%htrze2VK zj7ha(N82*5Y!#{xY0M{kDy63hQ(;M&BfJB2l-e_@p`Q4iYmf;*qnh`_0T5B7S7yCT z!_2+>9lR1u7(>C z8U8j|6BG%*H3LsQ&-~Zh_KJS2%q)@#fY!?^&V#CAug6uJ`;W{P3*dF*G?i=lxQG}d zFe70nK8cPmDEPj^;sCldlh3axvp`3m1<>0xRC$%?E&=Bok%^zQFpgS+q1O8;w~yM7l<)a1z^u-)?0%A;2*l zZ1DCj1%Q0k1eCY~`g2TJR7wq@6%2(Z3E`km7u{tk)+y98Ihy9saMFHJ`s0>dQR2hg zEZu2Rjr84WbNrFFc8{RH!zJ!necsjb%PiiDVuQeZrUH2kJ1#I9DC627zM6Uurc2ye zZx8}>f=!+Eu)lUtxp?~R?b&eHvl3ttktP@ga5Q!y8V98I^#1Hzz3ld0K;reUmq=K z2W8)VP4#Fr8+{iuPBqbKH`ft84?sQ#7Kq~{co~HiS)8IUAQ+HZsz8EXs|YS_jZQb7 z%3p8x&~I1h0n0A}+)Caq*QQ!-0R-?SzVM`iG@9KWym!q$8dxp#Bwd)wX6C=rmW7EE1rG+0%@ zv_Z-uUz;>uOxyQoaKQzcBgF6xIO5~{&Bg$mXm zSnY27JYqtw@hgcR2@q<|uW^D|<{I#06IGKvD2o@dh-vTX5e37?V06Wiij{^I!0e&a zUw{{2&t-=$fh@&vRz3$=Oxm!nZC|QnT{WaN+$!S>!L1p>%>Hf2dW;r-?8OB`w6f;L!~fZ@%a*J&V-90KgID(mpsKc0Q+ zFMU5ed(F_ZX_NsH2*DPeNA`I6=T> zc8|EaR58=Q5l$e{e18oLAtX)W{dSJzXhcwvq z15PW0ItTI3kIb2bY@dCTyvpE+S1E*=(jFW-HJ=gt$YWJR=mVF8lQ7SbIs|vxVx#WA zfTG_zF!g~GmW`9tWfc6~Y{qJIuV}4%J#-n>b;gIxZ0KAQxyba#Du!Ydv^977v}_U?ms2ha zOkgv+YPlo40B@Ck#|vz%2tI-XV&l#kOSisy&m!k__qIYE)7T+hIGkauq!u^%_~wsr z;04+q!YVCVn2*VSkYAg|at%!%B~Mx@&FHH7#sATXMij)woH)zDOio3$@Z-k;RqXc| zx>u6}szyoqxu=n1^9L%U1d2l+>$*p6=pOSL*rQ@o5W5Tv?=8{2HwJ*h|^y5@N!<_Pi%RZRdgE9aoGG+HwOO z)RSpODo2i<+d2R&C9$QD)fs=<1Zw`;K14A)7TSC*hum)*ScJq|3jG<|DAtCo4C;B` zLZfbq*#vpU&Cg%!++aULrZ%&bj}n$#DfSmjT&;uG)lkNAOX2_(A_#>=BRXF;!i+NW zem=L3(}|*}e;#HSWd%aIZ##VOJeHh?t2TA01XBBP0pdPzQmnH7l)E)tb3N?N#AWTq zg5fVJQVdR5%|$!<7^IVtSC6hztynL-1rF7uP&ha& z!CIrM-I9pl!kq_qj~r0@%}B2bv@oWCf)z^r1kC8ht@MUEWDnPOvYkQ`MXl_Ebqb!I z%iFK>_9m4{P#=Y`&A(=Z-v;^u(T=`RX%CL0i1%gmWri#CEnvY6$ataNm2*oQHmWHMrISv_DS)*qRnL2y z$(rb&!^ zxV7_)sdf)I`|X-})&Ykot1j=v^Q|-1nrU|-4#3>0%4h6|ji{sMc=PSR)*`XU8nS zGm?Y>G~CGo@(wU7vo-q5c;rZ4Vdtw|2+b-f`50YSaMhSjeRvC2<7j8}9@(nAg-R`F zd`WxqD}8}Bgr#UlxfC;~=zB1bvM%z-ebDR3ygO;%O_hM78!Q;Dl3d(o81Ioc=#OiqS;IP1F}ujl!Y zn-V&MTfX=tuReaC-!aTv4GvXe{<>MH*FjBM_!(& zcKoO$4dIVVYH0-y=2{@EvocfO`_cywkI8Xs+G@M(eXaQBiG&}~P|t-Vmr4J1V~llC zoy=F*2kXT}?*;#B_9kb>P_?_14^wb2K|Z8g(TbQ@ zzLI_5H`@*^c-l`xKRAleS4)wZL=0HuM7^eFtg*5f%6Xn3YPIBty%Z~n>D`?X;p_u| zlGjP@XGOn8>f|b0?h`9N8MW5%*po_b4+3hHFB6E{)R^A{7G?Mr1d+V;@F4opAu#Hx zR`G(Qhy=Koh=OWczuY8*`>Ho!yVcmQbiHCBw=Kk4mSMKi_mXgV0UYEzJ_mu#`>I7457Qex5GA+$Hqg z+NC?8e0~_pj;0uqc6SWdXEX6t`sBU78T(um!Thg!RVm7cJeLa%Q&$#7x!STpA_pno zn0rI8Mb+~OGil%FwVn4vguZ)+wOU=YZ~78+0k&1D^eMlfzU3H^9DkKoW5(pjEgnpS zg|91w*y=7wC`R5=4e994U-wnQ5_TL~mC)eeiLT8iY+L(sF5+Joh}ocCnLqPkIyu7LcdO2ogL4xR5wN_^+D z2bj$Be*2e@uxUvrg`$@Fv`g;wXxNQ zRMc5o+a!?>2IC_5fTS~ZuWkmo#yiX{0WSqGEwAeqT&wToLs#**Abu>@at$6P6$Y$> z**+6uAz$hcWThL?`Jha^0kOyHaK{vgCB)DntKbZ-=5h9Br$nS10-tAqAJEvxZzJ*5 zrs>UJc-9TPw!cwiOqbKG5)+hGEOaNY(Ui}!^deKlY?3t;DV$0os(W%R|0?uXfFc^$ z;Z$1XxkSHJqtk*ao+!V+Y(9!gvW4AJo<3~H! z24ZlNd$d(Olc^oCOPOm~fH@Z1Su=1?Q+i`FL2zV{vWm??1v&U>_SZaUv&Bq_D+uM9 zWD^?k?JN3ko!@zKNcVeyW;sizFsa+;@Ss*Rs_lp+etVk#aLTfTVGUFK&4oE+Y(}sq z>M9W!5@n2f;;W^*L&Z+N?FmeZ6aGoR4jM1?tfGv6n`xREi(#hmLCR~vjZ;;D!w5<6 zlBF6`a+K=dtQ)jynsKVx3&g-@-{pUC2QDvrhk%hpFC9fb-Ejx{(=FN%DhX7klW~h` zQomkx%79bY$#G9bg(C;lCx(ef4)Kr}|5mL=-OS;-0>X^@LGAEDW~yD@m8z1)gZP;# zJ1tSe%sqcq78<%$ovoUNd+8`e-bg0WC80y`S_o0a99h1D{`QC;kwA9*WR>8~l8^0T zTHr~3A!~RN1P_XlsyUuNj-66@PW|AhwO+?gru5GsCz9Ocx2x!RII_uY>cyq!dqn25 zSYH7v^RF_;rm!c!SX2jgp>gA5q5YRi?Z|{AM*1iSV*WPO!%~|=Li3+m6{=z@^#LKF zOH)mTRmiKL5_O{vh$#DFKEN(IJaTu1SKA}>*UEi!&Dfsg&&v4+oUshR`U+rQCut}o z!hwWENP29^vK@Q9)cvG)Ez20Zfl;8Dno?QDY&-CHOgcVS%!fw zC;BN)cV_4~do-`}A_@Bvu(yfALa1aXAn}kg)lNRjY7Kd|4$=)tkpETz=Ipu@8mGtT zrv37XkUZ443C#E&sSu5vbK{)$6e|?1gl+pbFd#OKNdu$0^`a>N@~yYsga%fo3)WO( z-}~(-7}%yzlhW3M0bu_@B@=1Z`mE=i8CCW(h_LJgwGs)Bwl_os_?5gsW6A-UsK7Zr z&n5W?8iOOB*Jn=Iz+|&oqV0KOX_$%g)17*|w99-wc<0Voyld01h7b!oG_Udqbw1&b zad}!-@Y5zqfQaCW6m6rE@gxneS`Q}^>|2{VUR>J6UC&ES1i+a;yScjBQ@W%`$I^K} zTESo>0B*jpF#!nFQl4|9XhW5`Va$j(? z(B;+7ZkvdXGm%=<|mu|qR+&eCi~+87iSM8=hMX7&x$YTIveWQaQNuI zK=zlqOae42;_8zxkFW|L(kbUwfd)87Rx?sj+G@`J<(L(D6;5Wy@)(aazZ)Qd~fkcE~DS6EkUNM+-Giqfv%$x z3}8JnRhT>dyyEZmg@3e+0$!d5WEJ!`%j?gUn8>R)UJDN~c@Gb^TbesHb|)+QreKBA z;^V_R62HBVYwE@{)cUY^Erw)#oga@ZLK(P7$!B392Sjd-96z8sdg%KizQ3YmZUuor z^=|6=UJ--s10axdpx!$WVrT+t3neC7Rz?-G6^u?F7Mmu z0^J83(13sE#-{%!Es5jsCWKJ^5 zcFb=hS4E$xpZBn^{`&Tz4H&aWcU#o!9$ZUS4l-5^&aH*>*nvuqXwM)}>%e*`CG6Pm z!J$31%|rK)gynOurLU?3h>_Dqqe=e+Sp8m0ynbK~R*L_&KX|b6=*mm@yfHh|468fI z{cZ0`J>LvTf0%+X`2!d5!X^Ne|4Q=*VlJY-;JR=3+hPuiHXg2<-I1(B19~qSAO^lQ zSoP(afs#>|3u{M&FW*srKL&D18$=55iRGYrC}6-f0!0=7GB*$1xiH0?5cgdk{MyOJ zU@}zF_p0yO(m4h-=*^kc%K%dA-*7Cr)Zl|TCp_UOZ5hvHix@zb0~#m~tc zFf5NPQTs6YNI@0At~###MpUhlPyh5M4Dzqd8>Q=e8=h1?wlE~Je(KnJn$8r&GNila z6e#=MD4)K-BwU?65BPC8K8b2*)Y?^kEd00~2jFo1r_^7+m;7G_t~kRy@d<^sZ;Ia!qX zvHCbbMpowXRd0hazw9!llS~I?GKmz`Fu1@}U8n~iB6)w+FoPc+fLJ4Nq0iOq!%7{56w(=YiJcV-Z^*= zeRj|D?_aIv;K9?a1b^lwr^;o0E{jAtuY5+W4odL`aIOKgIDyklzq>^E2t$g-*yS@P zD&G6%A&!PYP;7b%SVMZL?JDe&ASgzdmt|hMul}+qKK`gdew9^QVya1{ccYZK^iM+@ zBfT2t{sQ|5dF;b(2hRb8>1VqpP|mc;Y;TwN3j3X>>+tw8Qx2L&f^T=s)tqnrj%(!I zS91V4qNTraeDGAy_v;T+SAyZ#FAm?=hSda>pnO4%eMscsdA%=%sG>VXwzW5Ug%?@R zvI89&V9dXKPaeUSLVa2XT{!4{Xe|Po88{AV;~s}eS1Q|wSID52h`#xoQNr5OOq+`3 zvm|h`qRRk0v<1|Cn*o;X?LlDj-4pf-8=wd>Z0DT@`#qG-pzsr=znk>G9&NC=7MJ)l zO*ipw7a$F!d&D@XWy@rXcUw~Tx{3<9>C>uO^Eq#5JNkCcMB~1?XUi)L&>A?A0iztR ztA~+@6-$jHoKrRzrGC18zni~1HC-=5b;C>cA^hc6Z}}P-|Ll_e=LG**9Q_P=q4)?H zwHuAgg8R2akFDg(Je55>O{$w<|H+m3$MFle#5!(OnweE$7e_1&56@^Y8dM*=Gx*OJxVG??~Fn2je&8;3~ntxg0=LovvKxN-80`)>b!NB(OC|F+#q(i{zx z;EogT$BFw(+KtAT2b@f#;}Ya%1UDiPcs zVe3iG$TPd#e*D|Nzq-tPZ!5{`{dnXws`1Q~L7RX7pMN>Tdr$3^c!acYh2hflgX1fsF7HtqAKF;G`<<)Bcf%^RUdhVYC-|KQ$LH==act2bNC$xWH zwfHYj^N(%*@pkWtVYX7$x3l4{3mZFRXNc$SHUG-T%$OIplB!C*qiiv_s2jFux8Ywn z{#&*MGOcbw2o#||!5{XXI7|{Hkk~%4AS{3CPX2KR0v3e9d8RVqkJQOukTFlAPfRpH z4p2jWz8-(uBBd;)lf$T$@{mkOH;IX0QXwzaArI%8{H1%xzu(zlKYDk*>dW4g1K|(F zpKc$PRQWel>)(E2!1~LDd~GxNrW+nuCQRdvfS(2||9E~SZT=(9VS;&(LEeypDUpy? z7KGKWEsxON-KClF6>K>;MOXS)>cY%N=&#)P7q|FN-H|d0fWz`}*2t)Y9fCzAmCpK@ zt(9=AxCp%AlYczm&)xz?-CTe~c862bp5-GmNM0o|oEiwR8`PG{UpY+LCj;!oUJbxX zAjOWRgW?E_&qF(bFnmnH|3YGb3x!Dp@Hzj1IPL4JuW5x76VUStmd7Oiw*5bQV(@nn zc3X^Y4Hm!Nf&SvuR%A$A72TLwfB#DmENwsL<6snew->jyLDoQ0Cn-s^TDSk_&iWsZ zvdZAgcurXlz!fZgC8X_tDf}*irx(1*IGAn+8e)Y#*$I`blh{kZle(-~JDa@Uyo7$hY@! z9dvuuJ9x{u_dZV%04=GiEk*vyw>@cA@67%#7uS0&S6&Q*>I%?E{*`;4T)}o(4Fh>g zV(v=mB@8mf4Lde9{~D%@-xSmi9h&lQQW`c>0;p$9dAx=mlU9-95C7%N{&o44qz^vc z*1r`t!D%0MxGrHE)peb7r27}vcmGZc?#O4<-B{ij{RacpXTPG({L=SaXSIkk=Dv+> z!upI*8g6}Yrp}!G73*;ST=>g{;`E_I`J1nAg}TolHQ%%G~e2wk#%5xBYeXs$Y;{f!`~`$_=Li5>-Rg z*))4WDs?SXI`wA4)5!+k4{?^ukk9cF9TE>KgVjyyUqD4NqS+6K)%CM|-Z0}!|zlpb$wX}a#0BcKXEy@@9e-}?_^&oHpObwYleCBtf zy)uYZJ;-Lk<_Khf2e(vA?`BD>Zu$#Ys5MkRg}1sVd7j7oV4T>&EJS@vYpbi_KqN%( ztcR_=mDI*+n4!~F@(ueh)HBH4V$HYj&UW>;8hdz~7Z{B3hH zCLk8+THG3;lJ>f2fD^N3>A1Uj#IY)O%GcuC{zVd(9V;5?UJcQ7un3h+zUp3;{A8kf zJ%-}7JTT*WIXiF0j@qQUH@w-N4ljJ(a;5E@7GzV1l7v{syjn?=}8h!jvLZ+XwBCFLA`#w0Kra zVmRC>M7)SvxMf<$Ua454KzL_C_9h^$@hO_SAmPw0Hc8Ynh_mC1aoQeVlqp?$rK=QD zMO`JgAWh$MJw`!&jTYM@kzaT+PqPiuTtmD(NB5^_GTRz@(T%F03uts$v?c0%w1DbE6a7 zEsIXsXZidKy8Gu=m==|iCZ+Jcw6nn>y{Ti$lID|i2^q>byFdJyZsKNV3jVc>tOI`N zU3-UyLkta%Bpoc-oW2kh_fXN%J%+|YIE>r(_R`ZfDU#C9E8IT<`Q&Xod#=6ZYH0S* z5$zo?NsiOGzG^*QxeY2AeQT0=(g)o-Fjl<>6(iom`#?L{>l38O*x8_Vd=~y3;g3#t z^Gz|x^&rUBiyY~3Gbm6E8ilBCqInGHt>>OWbLr~`W@8*xyFyJcaLO$sLU~GAV^xHJ zLhJUy%?qyKEhwkNDCMR5I|ZeB?C0uDLoAXr-D*Eb`o{#HI&&_lwKw~k0HIQ+k=9;0 z)`ZsbM7+nnLy4>=Syz@Q{=EPC!@EQX22{LUz}#)iSEj5RQ>Kkgnk&x_=$2z;TxVSu zCq6F2v*w1>GX-VK2dIPD`O=wnqBFcti4rHZJ?f7ZeEMQtU6^!T2G&(=SWydmYA3gZ z5mNhLH7(wu&>a2D&kZ&n*Og;Y&KtvL&(51Z<>JvI3*SZg47aC}9eS6bh>S!1Cy8FL z*Fy`CG5Bhr=6C+jj<+6q>K34~4d7r?#Ey0R&k? zU}QOgGVpDvWlyoIat~#(ull1S=I8y4og2j^lUpSzSNVdswld=hwP~;7ua#(}0P7ZD9Xo1FN|9*}4LmK>I)nX%6Ltyf)exXzsUc!nKts z<8P1?tTYQbnKxjW+d3P~hJ8oEs?p>}B)y3xJ(;1+-gR1N&Z~0|?x03kmi`w&KKDKI zf*L0CzR&rz0jP?8RDxJ$I2;-LB#`)IuJXQIFX@T7UAGAjP6 z=eXi}_uYZUVw?pxtQFy8dg=%P+e4%p7YE~w7e=2)TK3~0%`1l?%?cV2^aAC`t4h6B znAA~t?_Djvl7UVwKbNNBAt~JYJl0ZTFi{0NTtsm|EC>#uWt5uNeLR};btP0uIs@mO z4s9Syyy=3A>qd(2WlrM%meKup#JNLYuaHoV8IoivP=J2BfSC{3j(c`tefK(bstf4Gb!gQ$s1o_-uM;ayu>VrxK|Fmc(g%(DY3Db z8YKo@PP<^^9y)5Tfxn_w2T7?&F0QeBlquPWOG_qJE<^n2O}mQUk5z7nA2FbRQnBsB zwhG=%Htn69I%8gpg_FHV-?{f8p-Ybt?t{x)m&=D9x;>D>7Ls0nb~;=grk z@LBUj$OwsvY$EA+K?nYpWZLtx+vK+ar%5p$vcp(pG#1_$dIh8%MelgjVHDq5Bua5HYV&#u=OLZ0v=hylSI$T|U&cpaV)IT`UkFM%Ac3>!`O8yA9o_O;A zm_X6K!)*TuIsUD{wV-(tuxJ|Hn@SPNAH4d0S=1LxWPfYt0t{;Gu}TEh;SDTuAk%zE z`dm}wVXb!gr182kyXG*(O`BO_#lU6(g75xKy|6(>v9M&PB62D!f3<1$c2F#=L-w=bEyH=SFTt4Sb|p+7W0?C-up6}B zR;J{?zhTrOJcEqN8H}oWStk(rW>b!MCt^n4gCfIc-|E=v9$8=@<=H-~FZN2i21@lN z)Jm@BAu7djD22Q?JnbKDoqg`lN5vgMF3`O;{QahuGw?U2RXwhmO^nys3baG~xTG&+ z8?{!CQXBm6Oc%OWa(r+U z-D>}?a)+wI3ezVmdG}iMt5a?EMyNv)eW=GJnvmWA&z$`obE5xl*SE33L5Zx zrlW)7pi;j28n8Z=1l;-lD~lf{>Pt(!)`lXyO?xsI5h%FfR@g(~EAl+V01 z(s*59)Uwupqe)S82by0e_SBG;*a&Yv@l-QDZd3?G5hJpj>7Kcx+j@ zhYj?z!kS}@uPiJwp)ffLriX#D@7y*qBC#vJ<^l$N_-Wft@XZdf1%qWn1Qhl=rj*`T}j4i6m z7FJm%-W0A+CmE~+bKPqT?_Ik}HDmc{clL*42w2~{16E24=ip$2?Wes!u6=k`X0_|} zM1A97^EM9c!>eX{C~Rw8i?Bhxt0og|rib#{N&ar3@~&5I@LgkhqWmJ@&U zrS_2i?y;LvqN=Lq^v(-R^QKW^?~NgI!)FrYJX0w ze}){r;(eS*BXcS&*$eN6LtU&2fVP*III_1bl?n^ZL$-b5b;jH+B0khQ*dtKiU80$t zpO^$?KiDss9FsjzIv^aWWxw9DXhWw;bFV`N6^j3dQOkuef4qrG*cU@&%?V!US4k69 zJ4}i#kQZ-~&-Cg|TjzxO`5E}_MPho4ir@ED=Cn{Ih9t3-Y9TBd6KycsBA*>$4_9s# zdGugvS<3+jTG>mIH!+kl5Ybf?U&b_qP3&)A!FiZ4Klfm-ZoO@Xs5sxmTVC-#(oJ9n z0qa;nUdVbOme$i0i5F|=F7tpz$Cx%M%Izh~6gv#rY-SBuW)3X<7JFQ2Gd|;1UWDF_ z-n+53O7B-+-4KlVa`T2}oMTPSr}fh1_|(+-QAs^vZ7({?;X}`r`pz)yXM+Qe(Lo{q zOwE5pjo)9LJiS*~h%eMI(s@g+_Vj>8Fi&iSyr-6P&BwuPhmMH!`)D=qOn0A(gGQPr zZsqk9qT5)`{GOt>7ZJ4lE+b;1fhBI}*#$$Q?RzMoQ(?Hgibf}ivvsoybbk%ePODNi2h*NEC*J7$ilaZBGslh}AGchuQ zuXm~KWl~yBUXEba5;oTZnf~&s1)m6{bTC6t0AIFgLPB<)Z!$x#SuHy&e|CKnd=e9R zePKv%`F{=OzZhb&5t!iQ$IYv!rWZ9~HsmT(L&=WNFjTG2Ox?qXZ*-Fh5MEQ7X z;_h)B^&=#BNdZ5#$bR2KqUJ`DXknk|97>!TT-b7G9D67y#KdBgX8VM1X7&L0m)-7Z zr3;f(XusT1_iDXHw(%kxLakFACYSm3I`;Mfg6D6}e49qRzqC||c02ompW!$r$I{jHs#Zu+BG6e5v!Z1=LFRJV6>PRA`^o6k7u)N@RwDy^kv zXDIB*9Cp)xKg!u$ITdJ0!K7bNFpG^dLr+fVU#BjMvKjX-$;;>$bcaCh@EE)`>21Oo z50*Qmi8*Yx#X3;xjU)3-@e=sWw#W*_Gz3(Z{4& z;i^%#L$j$nZa3>XbrbL0ehyH4gcMUcVOu``^}LxP54=8lIFOA1KwWHb7!75|z$XDs&_;${_~!6^RJBowV=pf^PT70_ zK;1WLgMdPN?GZ^^4%|bJf2qat9}BQZ`o#OR5IyJpVun{n%Avq$=S0P+^P{}&*JEZe zvHHpK;Rkafe2&+1zE=>LjV?h$_f~pUF6h6p|MI-32dQT`zr`2-*-_Jz4)$Zs3`yOi z=bK#Y?z+Z@4^mb`dlm$ei#|lb14dZk_o#O$y<(d^DRq;5+R>l=d`e<(9{v_$7hMo% zoLv3@GBPOO5ISd;qh}@O@YMEt#fZX7>clq(AN1=M6gMz>(Y4 zqLSj3KJw~sf{!oBl;7|`&Jm@h;NOI~H|Ps;%>5p_hqR!?Hssh$$bk=glNUBh%9<&e zP@zpzWB3LLJvK(3s*5G^eBU(b{o0Rcgx&(;GqI{%ps>`lh2`jrnRy@-_)6pT*;;@S zvH}qKC~}D}4W4uuH3kk_E@Ya)QP#)M4+h};od`M@-1Mz6jhO&sWxJf(Z0`cJu9V-p zC%{9*`zHZjBRZWrq##=bX5kEZVrLX!Lv|H_ma7-A^NADd_x#)b$OdRX%?c~PtA2o- z$QfNgJ=8B6j~&{7=ge_MzS2Ti>ie-1PUir*&dvBt`Gt)W(YUn8{;>Yy%&16*Y`8l1 z$U(85#9ps3IWsZj-A|W{N?gz_ud9#AX(+Xu#tce3OkgZF?<;H8_jl0^vU+WSGo94x zs8pGZS&;k!K+Vj^-%Z_kl#VSN6v8!NY{P5MqnR~_Z8 zy^}BLC|F>n?DsHhRmJ7^$={X`L6iJQYfWKJFB}wQzPr)OY{q~`s~C)2tev5Nv6@~0 z7$JaVD*+5RJz$1ps_82>L#G~)1RnJ@+tcj`@HR9sBe)*K1n}C;wc%f(obL=QD^c&& zW5h?q7dVA=o!Qfgg}`FPd~pJ^GD`tj{}#v??l_qSTGFDi)ldz$GL~bMw_MiLlGhZK z7!on|q1waDPd+c^Q4S7qsT_D|+=&F%du1UI-;KJ$F3LQ6Q}#p1n?&n&`r`cN3UG8K z_S$dwMJu)Abqwc(Y1IvpPyMO*_i)lpkAgE3yvBu0T_@tp?$9QMYA1~=4P4y{FTf2K z-3~e}B&A^1Pkp<(kzVGDh{d#Dcpjr_f;%x4-)1kTcdGq{Nce_qf9dy(CLo?|3FypO zb`CCc>z?5->r5gJl%{o{6aUP2RbYI>Xd_vy0#0w@IBgY1>u zS*V7gIU{#+`Dl3vlEPk833}|Cw26)?;;G;oF&ap5Bz_`EIxLvteNvZ~%DB}H`?BM7 zb7hdsZl%%gZl!5!()|YI>Lf+uq@>N;V^hblA&K7#^mV0IKl&`PQaxJ#QgHx6(66qyGs!m zZHgAL2_~Dih$<9U=?U{K%V|1_w(W5+=(37coFA0q1}XXGHzn3m17Xu|iX8>;u|0e% z?3&5V@Py6W&?nI!*+VD3bHF4IGwY(DZl#$v8_R$`V2yx)y_!lL08GNEHB}~?>x*IR zb5ri|KvKIGu-e;$VWv5^5@0<$fRX^K;`7P()#ER3_@w#)^2POjiZoBs#?J$8Z6D^3 zAR@G9XYLQGL2DbIeDO$0VP~mLneUulOxKh1umVMA8#be{$t?zywJc_C`hpBsa@YDW zOzo5*%%Ir#eNz{;##dOig__VH?b>%LjryW%r6@BpkIT_EK`+{#Ds(YHPe*A(k*g$k zcO1OOGkT(GB}rsZJ2kE(^^Te}*NUcuy?#l2(xO6{1&Qf+3^Z6F8tdqQPqn{oHKc9= zyP@w`q=zqFjpmg~pUI!eET^SU$nAl%?{v41xhKG^@x6P}bAnk{e* zm*pdCgsGdxVy0C-i9EUsCT>L+9Qrz0Hu=29_nF=@*-Gxot3y>RsAU2sfW)@C-~MHF z*0;TV>#?Sjeu+@^vchzy!!d%%detHHXs8j|t5iwV%R-Q~JrME2BJ*`(6Cp3WR!0l?kO0h1~%LU;!6qqgW z9~kt;i|D*y@H}<{p%x}|RnLJO0mfJJcJs(|F5Um_ z-*V=LCrYL7eAbjjD^Q#JVOpo|?)*<8%^y@#NgcEUtl|+vRj{Um3|%=9lZqZ)eUn-^ zb3U%bDQToMuts0KN>gS z6-7J1`TA=6G9*Rbp;1Iw=emfmoi(50UpnZo6@1IvETS*l+J0~tDDTQa5PH7q7FPm_ zmWSJ~W3g|qu;KfPG7|uGgFavlP~=+&C`v1pN4xR> za1f;~#qsyb4+IMQX%Lc=nFTdsEF~;kTzkiFIAq63&?){yFUZjxa-K0=H4Lr-N&BAm zwy#ydsF+4U>G;kJb4I8)+dfdGB|rj<&dy0LK*|poF$%UF@I2Ucq=vM4YmL7pRXI5y zADI3-@fHVbzD~Ktf$)*C^)i1OJ7l0)jBJu@QQild?oPV>h%nT*r7DV)(A0E1;=V| zC^~^Dg~irE7gO3?*|$6gBmjWe(<#YsEdz`;dX4N_Dc#CZ^92M|stI}lDR`_N zx7|A}%;f$0PPNLcB)u(W1A!q0^QsS5ru)?Ne;)%!!JVPj2l&QRC?n8wCnD>6eLNCAYxf2^t)C`_WHNfa5$)0|XF&nZ~iZ zAEwRmUTS)h``I_5v)ErRtoA47rQBs~yO$wpnlk;(>G-53d2&zApd7sb6n1;t2XL!R(uqY@F%>43oof>?a z-;Z3Hz6(OrLLOOo53o(v0QQa%f2J^a(f%JT-v-4kbl&tKo@Lp0e$k!e2wU%DqiVZv zf2=G&nd$o10FTHp1VHxn@IEaGzC_%YN3BQ--}mR*Z`rp0AgF=y7tp~t&w~HS>np<- zdGL%FI?13K+Y<0Zn!S(DTU!6y|MY12YrY7+~n(wtDwGP8a11 zL{JoxtHJyPH=upcNK%kDSN$jAk>l}wCbB%5Z?-LaNOD;gOqc(D%N7J*5G4Acm!WT# zb(wJk{yu@|&DwRM*Vu&hpVyGzi7B=Ks$saJC042tXGXGS{wh{BzCTXdE!7tY8c4wj zw{?aq8KC_eyHR&wb_4x>jg5_PKyTXvhRKuWr1fH}7$!1Jiw{tNyk@|wDPWXhEo<#^ z5M^i5^U%MZ3joI~+kf$$ofQaX<9)?vB^1+;!Egsx$vrH8M6F8K_~S=-<36K2(RT=x zc74Ae%oYUOEwCe5F11cAy{Iz})~$t4^^|+qA%Q*!kwK3k38)f4)@}nP7K(xR*UJ96 zSfuaL^j8XyM#TvONJdHrkb=Q#0{Mo_NwtH*&YNItyO&(OnV52J`X4~tTs!6^*;GGl zCpq4tjC{xL>i3ew=`|*S4UffaSww1iZh7X3q*)Wk&+ydpXj3hZ7){4GBfb&=UGMV} zFT;bVP4-;4KMbrH`+?g9_o}K#54Id3-Zd~O6h-vV`1+O~xyz$)S^=%@K5S?o{K^*abw>6+QcPmKyqm0g1(gop!cb$_yY zm2q6#bE`)shVVwZU+>N!U7&M!hGPDQ2Dv{^(OxAp1lt*tiq52bmz3@3@lIjx90MSj zqv3`=^)1lMN8gvzA}soF&TnNVWe(oI@#mP2Q~Hno7>Z#0eh$fiVMNO?qU|l*zn}g0 zCqv)h7h1S1aD394ng_o`43|6e9~>uI&x}~^vEieasoR@9HDU3OLv+QIe+4SBGEgu* zbPfSD>A*kNp!Qv3W$so9VkslkB2U-Juawz|-oN?7$&Iq5KPZpIr2Q9StwHOLM_WG{ zm-qVxKCQ`geg0)n#}s}SYYW$@{Upx6+2;Ss6@GUh1m=%3w;a`)mn^7r?})K=^7oLG zv9JG9Kl))Qz^j0J$ACh68Z3`iqJ1BV;EloYhE{X2?RWeo)egCk!|FWzv?z5GqS(QR^K- z$}ih-4NQ-yXdHuA-V4IlQVJa_{w*{9+xNVG;9CBE);#i30G!WW>se{qRH@ajk0<_q zoc~cA|KqKa3)eMPz3{Npf=N-6ym2!o{clSB|1`9J3(5Yr`&+ghXMUOe;8`wt*F-^Z zL0^&3+vuoM>(u*?Mb7+T{*-PWyj}42^bimA-?7z78(?b?6Z!b}`3odPn+zhxi#@+T zh`KHf7=kL0!juv@*nV9!Ma$#}Cm8w`H+z7)8T1?m_ZwNXHip!Mae6MjV2 zJ=c(JP15F%?)}WfB>%SUW#jbT-FmNP z_>h%T+!zXic%Tc=?PTx*a4)<(yhcFn1|!sAzH_xKfTf%ayEp;T8N$mOqIsY`-gkuy z8gW;Vn^j+F=p0C>Y1aU0n49}Kh8G3n&^M7;T^*>G z0i-kFP9&C1B^CgNhg0xqQ$7%(B>1h(6azZ5IX~acPN0ATSO<0&fC}sbDl?mxwAI<| zKgDD*Lxv}v*&p_C!oyxyLG754u=pR;tI66sjlE~yD~wSAu~8@z3RvUjX_1^+yG81K6S8w~@=Su&RgX}Xl>+;HhwqWib&3O%UdH83RR00#OWR)W!JI2gsmO}51O z>OBUf>eF?3pw<*bPBB9V;&ZAu%6-8k9HnL}`)$DGM*x3Q49eTEi<;c7$kx)JynR3M z9LY-Cd>rbMU;F4rAumqJpO!G?$JSr};i~pFgx_aM#N1pqHrL4`&~HmSkWKEB)@y+ME>Nmo~}{m zPKHRi@Fw8CBg(@QDe@x{rytv`gF@Z94JUutu3{FfE0DyYU5U&x$d%2Prx6PL^cIU$ zx8sV=2?;eimX2(%Hl2D6%E+eOVc=2Mlq#ZF_8hfV&V}M_0m;1o#fA+q2mU}RmEKI0 zPT79R)BUXC zT^?<=o_=QR0IuE$*-$%D2`51lwN{6Y0fxyG$HTFc+SrOYTfn0Eyid_(^l~-IX8UN7 zD75@_R_F1pq&ra|Ux>h(R;bWBne*yx_YYozprCJJuc5j1Pz1a&iq&Nu+ zWRNPB`hsdR1#~ZP9$2VV_(aVKSxaD;{c1P(ancJtSy7UTB}^O2xkbR@raE|ERK{kB zkF!of_VVD|hZ1BF#R4~?R-mj)_=}b2t4mP=rKsWC%M7zaUmxmAHRtF|0>X9vmFo*l zf@7XB{k4bY2M^Hu&BOGHev6!Ck0qJ1n@vl)Pit?UeVAWn9*0@01_P%fEi#%zB9$gy z6P)CO77=*ab3ob7*)-CIy$YEiFF-8(n11B8G3pCKu~J4!}89_&wGmTP-VMiU%=%=1SPJe099tUNXaTV z@eZGLH_}2!`%dqO0n|e;wU)xD#tu7`xlVxU>iGN}{Te?&D%xFSXJ7}e4}qkP)eEff z-`v-^vA%yRFwRD^eTm$|jKX8Vr zzc{~>3(y@1O`{iHFIG{((jWA*k8}y_@T*6f9iL9$JjB=yGUB-Kp@%ovuht z|4V*9F3XQs97n+Zkj7@hZAjrwhvA?=H}#hSovTptnIVS0W;ue)%ligcV=2J#ndBi4 zI4`TAiv|p81TmJw6|reDJ*hlLZXBKpS9WD?J3(97^35;3I{zb3A!(Sigk8$qR+0aX zGeTF$L;knD=b0;(F&`=cOR2)%tUNIK9RJ4d#T8yBN9wMG6H@8+<$*&-7QwY#Sgf`~ zfZUPT@77jyP~=X}vEZZkOnsLl+j>GPT0vm=HM-Q`x+iQ2@;EE?KFH4LPNt#8wHC}n zK?^j{(Jk@6s6=OwyP5_>Yq9 ztf%b}Sb%D$R^*eHb02(=I?ZMpuwWi4EhwyG%Jp7&4{Jl_=hj-%g;aWb&{5lPWj8%f z?Hl74mpk&-hOU~lX1M%8;PGQu7_T0x@;A1bPun)o^5;dv2d47t1q#=0}{k>H&q_v{en^GKN-5J*m}IJ}n99OfIkYbc?VvacdFENmXMBA38h`9ZN8uD#ap{ zYS%~Vg6k_){mpp&P8*pNdn+mH9p$iB9KOGGU1e(?yZL1kqqFtMw`qGqTZk?)TAE=R zOMTNn%b{|S+g@JsKdA=c&S{2o8DovS-QIX!z#jWid`O<&ksjey(GBR5s|`0d(v)UM zj>`Ndi*Zx$GP26lzJNZ$c~a0e*5S-|)W%au@iy1jJg0O7^Yb^lB6t!E*(f%NbIC$u zrW0#jdlXiN&D=T}*3l;la6aKgat#iaGxd6B*t8-ehYg$=abFQ&(tcRqQ|w^mDINYW z-jAfXv68|^nNA67bzd1C^bi553oBKY(qm3v0P&wK#@0X(Ileqw(4B|bXd|cIqI&Ng z24qf)>@L=!9*5!h?w079HuB;*-k)TxO7oo8;6j|DA5r(q+gzy6)U35U@ZQ*UV)KZJ zPc{FKS<75mW)MV=Yn52G9O-~aaZ4B2eU+QGjUAFT`Qq%@)Ts6P?6IRKyVr>^Mf|r> zUIUWRU}^9@!?k*mT5Z=mT%4*_Q`=hgi{X|chZGCwUcIQ{(!dE@!vQo!D9*P&lxJg0 z5U7vQ%yr964cCMda=fOOKj>z>6!P@{cKuY$?7tw}gAo65BU`pD7$~`YVbSQFcygXu7T5{Bjy#Zzm^@O6@IYnFfx`J( zS+|6H!#$PpiY3;AR^6je#P~9cTZt9ihX4m1Ur-&yffO$bSTzODtl;f{UCpvhZc*Ky z3e88Ywgn*@W(Sw=tIWMnX;&=eZ@Ie(B84@)L9&tF0S-@W=ax0OCGC$S>vBI0>139R zOZ@eaoQnMl*^{O@H&Y&&vQJvDlb_$dUFQ1UR7Wo`8Bkobi3z&K2NG)Imq4=P?D$iX zb=Khfv}dUwXKf5gHqYMN=y*DmQu$X9FTCvjWT*iIQ~-eogS=U^5uF1K+0~lDNyRXS zvKB||MUgv&JJ>HENd0rJgpVKwTo+_EmsBjfW0B$BaxBrB1~OYh01NM&GJyfExf;ex zY)XM3O$FvpG@)4nNSEtduU7*H_<$^siHxpP8_l#yTPB@0PkQWg63>v>v^@$3;-ks8ctlL2N_pFb%EFTd3RUbL*%pMAeQ@ntD+{Exj z-=q_K%{xL{W1OgPsISeC+46Y)#=S`baiRrrA21HH^zUa?|>-xE7uN8T4tfo z+gb~lXlBL(FMA!srSAc&9FUlGyN9qsqi)SB_s%iMssWW9fT|23+Lc)z(xz`m-Z6L< zkCX4O6?vDMy5`j@c>yh+({-M!GFGjCipREzadn1)z8fw{CLC#Sm_EFPdEgoE(RRp= zFLue`YvP!>jLjI3BX$MIiH%oU3XO}zMwcXCN*;rQG=Dy3_o3!vId&oTwYuAtchVbt z@91x*0i+szvLRR|a7=f%QrC{rA~WQ;%PiN?xDK~3vMjhC7XHU8R!#ovGk9SE)8l{w z!CVc^UdPq1=W6+%3lPA>rG405l9_9)$jo2gw@|D0c8douPS;WH^)ot9lYKnEgMjwd z0-nLME%meK6f+cHtL~^jF7Zmjz1boNb3e&*^obuih+ScN>v$g2L#)J!W@mn0m-aC&E@h zDq>`5ND5+Mr}*vxJcWsP56Z0`#;lH0nJ!k&q!Bo5=$S*&}EGZkp4-bJT@L&W4TSls+ znsii5E5|AN`{kD27zCn~mrNf)zc6~D4Boqu#p&8-Zf){dU15{MV+Eap)Do$B95h-C6hmaV||ztSdt>wL!a>0>#7_BDXII2yr>3|@>fZYpr z)vBBVGH;qIomI&WwF-J<7iQhT5qd6mE+=b2GeKtcez7o@Zl-<LM&rHcZ03Az1sdS9+_jadoX?kx1Zbc77YVyKpNEN< zCq=tG(rRV;S%eb6_O4|FB?qVS0lb(pV)KOjY+{w}Ft&*q*yt@g0eN3AW$r{+-*}@A z9M5^iF#l5$H6tyl_x%*zwi&x2XI}4DaHFkm8073ViD#03%M47w%<65=WoikJsVTp} zEFa`m@jXk#Ab1Wrk6 z%5%^G4)?_KHZKvKlG6n`@G9{+wEN5`-}xQ+y(e<6P}AD3ZD544n`xN~0lYo!2&Kra zx{grJ-|pc+NJ%6cRLLM-Oyu~@SFzR3gNo>45*j4hl+E;;De5pjjgmM1iSAXPCJz@Nli zXhQAnWsAy(DH`z?8#OP^TPTX*$FCm{C$#14Mhtp{H4P(ML=QPv4Zv|CtCDBN_;c%7 zo7e47)f7f!G>@vyA-YbYU2-#LFPE--;CZ)H3`T2>m29#qgSt z%WznPv{dauU_J+m7jRAw&TK#pV>)W-0^$+s?$wxStj}<-!!}U7qj=G|>-vlB;?o)=Gsk1e{kUjCByM-%jWC2M{gE1sM%kBW5o zy|BfhA7CL+kZE19F+B0C9$ktyniC<4_^Mb-DDG?EPgabA*j*R6w5;0lwp)Sj{toIg z(X~;0)n`4tN&l&6Oe`NY(@jold|qz1Bn6+7b+>MNZ)l$L$ff86sYc6E#bJCHx+PAs zM9P7(90Tv=jSt@AzbRiYTE6nF^aP1vpbfFy3SSO zq$<_8FZ$Y`=+BF39J%uL{}}u7Kq%Mt|I(sTq>>~{dkB?e9h6F`NX3w4NK*EF8)K59 z2yMu&vPF!2FlI&xWsex^jD0u5V1`+Lw{za=yyx`(zULo3GRt${&wXF_wR}FG%Q^m3 z&P?id{V(lUV4@Z4hdNL&XYF8hD^%%aC!4YAl6QXe0#m25uH7dyB``9uj;Ka2_M*s` zMtgrdy<3Jbwloqm=e4LfR~s&NgY@;=JB=rO4XP`awo?4aRuzix^oWc#2UL+F{`22I zhw*3Oe&yF^D4wsUpPG*ho5x{9d$G(?Ou)keB74*y;y55O&Ek7k=m-lK=*kSMpW9dC?4&)a=dQ4WQ+LtDK;B;Ob_l;XD^H*#dzjUNCOwt* z?8d^CxGEOI;%}F%dX+JpU}Tq!JpQ$O47CEsC!?V8`gvbJ0L2z>cYi$rdv|{Evc&8z z6*~{*exQ7MLT#v_un@DB_7mUm=QjWEj4kqi?Sbn`1okfskZ3XrJIvc!zBhsiFGWk3KmP)HM*{ zx)T&CB$$)g#{=|18H1HycY!FjnywskJ@r&tdU|({Hg}9NAqY*^Bv!l~sgH3_Zm;sX zixvlnAEgqv5nW9sCyTvAKl{waScSv2ReL(#=|rp{KS@djfAeZLgr4=3P@|c0wa$D$ z|0j`>YS{n%y8hyPG4O=tV6`^Ke&YtNID{!FYhNH9_UUw)=k;N;{aH=l7wW&|E?F%t zXdD%iSlvhvf8`I#YJmnHX%`-y-y+%#0uL)4vG(R0b^f#cA=9z8Nukp(td@fWG^z8U zP@vQ=?%yAvHt_i+yX@6*(i?5&W;S2_`aanEYiZOOH?MN;UTLkQ`1H^_st*Q-84m z8-U4xNU82aAutF@?z*^>_ip+9zEcW7biCcS!j}up3VASd)94I_Xnx%bZqVc$nOF$% zAWUe^mun(Y-{voXgcU>d{mtSmP&R#ULcCGGQx-%(eoG$~QRX!Zz_~n{{j|h8_)*z%oHIHdq= zSjLo}!W^PiS+!A|mb0Y$H@eUsU#DiDUO!v5ZRms6K3k zVr5ZSV@Jrc*NQOliC@fW02? zn-&jM@7%cn5bKwGdO+=OyF-K@cO`=oYnwdp{+(BCM+*m3msTov1!$6A6|DzlohLIu z2SG?zAg@YsmGKZOh zDUoVP+P$epxUPQetyjkVf=Z!hhW6h}4bEib!tfTQq=cfQ)Y;(2kATe^BAn#)ze;5R zb=rJhb1A=d@j#r`@wBAIV4k`xjf}Xh?9ORYqm)q0CC$f>6V<}3(%5wdjBl2 zFqUz){(xaZn|rL!QgrfM&1I62EkqHTR|IMXgha=%gNioe639;%jnZ_5n4}i;pyyXN zQ;_S=xt_6R8sz?Iv7E?q-`=5O9SN~Pi33-o?jO3cyLReDwj;x~VT%&0USl^WUx1bj zdcJkw5b!p$$@*xyLis*z5s=`C;wyzwxW3=HrPl7ANoNIaR`}#6x{j(C^aV+;G7vTS zi52}7?f>;kk}q)B5|-`qh5JJbU}IVdb7OCtt%JZwu}oXyW_qPmA$0j3+oSgfiykp+wAa0R?NbYZAUwC_Oy38aO$L%oD!3yAA2=u2Zw z8e4Mx6XwU$-fH4UZvn)f$?%Acajs!@x`5x5xnnZorU?*?o*p%IDtx2O zwFQM_CBro>>I0A7ZwZ)Pt;TpgG+ia2Sx`)7fNqE>@hF5d1!CJ|2B*HyVB-Laqut!__ZvNRPi5j(# z!$IVWh-Un3Yi4~ouxl8$>QdFfezRk)4`m)9iWa?5Z@Z0hbd%dKe&LfF`$jF`Gb3xz zbuJbox}~-Dz*X4EACUFb|F#-uX~@6QJhlrQQ|Mr)T*?C{fnH?tI?xiuexhUMs)P%n z#CLwov}Y|38DxeEtHWZ>@^AO{oqKULSxl`29#WdP?^N{B*xS;875NIw9Sq|Z0}rnq zw|l-AIk9?8)O!-Buv4;{ry@?HmmQYASkITMf3q3S_MGWHsGSGd#e8*LuRx7lQBO+7 z*?%;nehi38g|=wi%{sAF)CDRO97yQf>hZ*nkT7@pW#bYoV6zdM2^zFmqi~Fy-%tHt zG6G+8B~89g$)$_S$xx!-GMJy~nX(_wZdw~VBT*K~-XMZ49g;P4x@LmN3#$jbi3mcjw@KH!zE>t7_KB<|n!DV%NB(9-?{k%c6IJ#p^f;H# zsq#Ibz=??mysDqO$DxeH@#_B57gn0|L{W^|SVQ@aprzU=a_!Z0*NPW&aqtRn)c%>O z$lxA01!1`cg{^tgHNbp8JWxG1T))_?foaj$c|h}V5OB=Zi;sgYWd(D7XW zs)8O<=rOOv9*KELKy+k@#1)#=1EfT&X~~tX za;zchjJ)n@dIR}-4`XO%xd6pHG&GMg$>#4A7w=>EB3?q-q)jLr|envw( zziR;AA%|WPE=qsLUYKtNTw&+VEW_;S$~9^xcpTz`DOy|MvedJ9dqb1ue3HNQrcZR0 zEN`da{$2mW>HOhwY}|zDrjy5|8I9Z5H0uQQig^CFR zqHer116uNt_$?Vh7J!OSwE0z(ch%v!vsKD5QWACdoyntHq4!S~mb9*mj~d{_iaIY& zD(0#qMwZOODq|M2Q8k^}D6A8;3!G&v=H;F&-$tQK++>}M|5ut%qHU>fcZ{t0=q2XV z-Vi{i=uU5pXPZsND>)W~`#E;r6+Pv20ZzIL`hd|t&H^{D7HE~*G;`@)KH6h;rZIAf zS5P2f)Ra5~j^3h|eKPoN*7CeVf8|u4_C~{vrDMXd3%Bcmd67S{*?%Dg3~xmiB2HqzI(cE7PmqH`O=8ZOHyg;OV3C+ z==!r!l)AJXH+Z(^l%xMCWRI6)j$M?rQJLGpo>87zd%_1r8*HS6Cb8zJi2Z(@hyoN| z+#ck)pNX|&puuqQ<%uJ3hc81SCAR3(v7o2m+cV45F3-j`ju<& zplw1elsG#^-^(A!+cIG=aX}kP)yGA`mZy`20L;QyKmsJoc$++P-V!G)su*-b zxzq64N?Hb0h6@7iE`lt-1tcneTk$x3%=zlJaVhii57Fxcq8=)hK1w;t1pKT0N|cTf zVEn0pu53Q?J2@ZV6}y@wfgiOD-mRehHo1^sfvj@Ev-zj`roL?EKIomFtunhGPzH1p z>PQ*V8()FSMI8oeLcgZ9Qst9Qzftrmlspm*&``HPU|3IOJ(*5eiL*gpO_Q%LE*hP9 zpE`4)&#DLQhz8p{A6QMz&yDEbJAT#nTSOI$PUV=^_C!&7K!>qE1Ej!D2d;i)EBJ;p zxebQd&>oBSETQyELpT5Gpn(lab6L&KeVV0Rlas zktr(|c&MgWzeSezDv+;F0ENMhX1?4xfG4$+$Fj2IVd77>Q&yTc1{9v41KfqoRM-5m zWP?(*!i@l-Ws2Ow^qhX}mQ^lKpwej|R67{A7duNl|#aKZT^&zJU435OS06 zSLj7(L1uJ3Xs8D>@2ksG)njKw@MdueEKmzB2IcOW6^OBbx^n0A>G{~{(;{#CGx_9q zS)zNX6iY>xhC?SNz$B2mhX2d!y%OYI`7R0d<3>izcW-z!1Z(%|2V-ShvJaCu~^g}2X|@Yes4c^bB;v@H$UKN+ykTgg~?1Yw(5lHf~B;nT?^N}#8);4;qZXv~=_zmRP2MU(4e)N19 zRA~dh%J5cuu!4dWq$)LR>TWQBn!rq2jKv&NUwOa>8fZyqLMIKJ*)#``52rdS0|{f3 zns>jUfnqjc_rrgtn}+pxkEZ-KX+peP2MfET{=Zy@WY{XXQE4 zH^or-q#!QXT>wC6J)}Z+=poi3!Q!if@l+2w5;>u|vka=OgocAw)SuXeYGa^t{B3|a zjk_o3xz5xRdAhB$OAOBqVA}K?-X%}Oj2a`kSSzQddF1l)45ea9oOESA2PB2QHm2S9 z+K9mQ0i?cdb{|XkGz}Mlp~Yp-OlG5=Dmmo$0o1G!bN1apSveQhMnvqDodN8oB3mj-HU*N^Zx7;%Y&Vx0Mq5cLJ>zQ@! zV7?nCR;<};C6A@K?$y1#PDm%M{^1i^d#5v7bGwv$a`LCA|1HhELt?c}3GYXm9@F~( z2D{|H&@1&v23u~&kvUYnbJs*%SU#qu91i3l5^w4mra;%J>d8fC(g5JU)C;p;j zmQuNPA>^5Av3?h|Y_#asyt9-Jb z)cSeHe>;Hx>({_MtryKU&^+_vAw)2S_DHubgxXhyF!>3V|F2j7m$3j=<>cy^0r8W< zw*_nqc=_H`{teajzu&rH;|d4qbP?pF4SefesKNhP8u)X>pEs4hpys+%$Q+i4!}Qsd zFo(U^S5Ge&xu_Y?jg|h5SbS*N{bm=a5j^v;|JmDpyaRo-mWW$TpJ)D$l>Hy3V5=-> zS!RB=kGFmQ{wcXxaY49#SdEUv*MF?wzdfEmeyujHv?I?R)CryZpY*H$wIsoq zwT=tBh>#E7(ya;6CMTV@KX?P0^uxLS-3LWJv2y$DWE`Z^JRz<0 zuMoxm`R;vd&#d;AxJn=JM9avQlSdt)x8P5J2; z@#+nTnT5ul+uo~}igu}6{`Dn4AIab6d+W9fn;vzy0gvV@`)a>={XnVOt$+8%oh}DQ zv;!AscgmI?fj^^@(c3*E2Y*YZ`TP6@rh+aoW*0Qh9N6(#-~@5MSKgo8-2d+&?`!3+ zEQdD^UJ}Lv%0<`q6`gzIv#oc{uHQl@|329pHkPXN25}#rTIw$knBM%TD{3|D!DBU& z@ZZC@zrS(CjJ;iJc5(UY3HXg*O!*3{H33r9lzH6v|41eOG3u1gjVtdsfx$1kI6(F| z5=JwHNRbl%W`=jz0(ic-$W?uT9EwNva1dnRn7?bu;J+I#-vQ<-jEsatLzg)5-3zLd zIDH`n?jykWufA9$5MU-oCM{m*9_b|CeAXr2WFv5D*v^>$U+sjud-v*K_-lk6H<9`OX8M2gE7(LpC1}46Iyv?2-19)%f(?2Ys=HJ`RoNZY^snyRHShou zIiO077B>S4i*u~eGRVJqz-}w7LVbBD3CLqNQqcKv$g}=mtir#q-oM=V#sgB}<^v0x z_byrn#SbO^JLB|?$BEw?dY+h8z}q!FF|b%9%aQFAMI5vLS0jY3hF&Aupfhy3ssg>; z(?+$!E#a`Tsl-1X;y z3zjNNx$CBOYnC`WHGg&e>(GB%^69TUU(0=xN5n;q1rC>D#7@s5^l{g7e=gi3!#t<2 zUXKxRR&g_RNInx_Z$=cC-YRkfng?0pUmr;S!iJB_x^?~bvxhHAnz$~0ziP0&dCj`x z-nwKJIB#YH#F?ZQJ@qKJfkZjP8}jy8v=g$f*^J zhgAjF#wxqMKq%g6kQ0R+_FKr`8yy#CC%k!=B?vqDli^VLrz_dyV@?UhPJP`GlYREk zZoVfM*@G0xj%ch`PD>4U$Cjt)H z9YwnvXpj%>S&Jc< z7#wNfkvpf`b!6hBS4_;?E#$Z%p;-9_^rvDWqrmoTLMMRQBhLHiq2^HdJ3UJqw_jdu zy>3cqhZ6~x7$tT6P=@w0XOpo5u|lP`Hd@ZyFJ_&}jgok#STxX`y9rjs2d^1bBb>jAY2lr4kbn3N*o}68(47XE?YErjP78xjzi8uN zM%K2)&Z7+`UL%Ht+wBK?5CZBh$$X{BuN*ZI=$go6ONBcPMI$oND@5xLHeoxx`U4X` z1qM3szFFBdSG9OfTbgGGJec~JcGq0Md%~QnnOb>xOW=d88?vMwfRYw(=c4&$o|%u{ z%5D&2xySY+3BTQz(@ud`gE7ff=BK@*BK7%-c%P6tp`m&P%k%OTCt*)lpk4kVw)(qo zHxOVw4I+ix%7=mefH453ZJ!iHz2w;p03U4tt8+KF53nIipfKOByH7bhvx0ev0tm^S z+C|Od7$*@L$+vdK3s4eocP2@Q?id$WU$RpBpcCo8r4;aF#=P3w32_|dmGrTkD0i(| z-u1&WVCw|ecW4We5cEU{_tPaBTR_v8JEUKPCi*wtPK+^nb7mc8E_ljK@oX)>Sb@5p zfc|DN)dqlWumULu&tQP2z(n>PANP{*Y0i7OWyN6Z-M-;Lboo))DWO*j9h&HkCBg*) zA9h(p;DQ)(PCX@!NTvOSeL)o2K-#UP>i*wus0PBp#rC19*qM9Mr{7{=X&=-{LWKa# zevg8s2b|fl$z$|Xf377nEyjF6As6tKZrLWe278j-o;4=1(Le_wBweLj9{^P0^WjiO z@E*N!zXy6!Qc^&27QK?v1f}*B88(@Gi*~R;MTz1^hjn5XEkE=va>lJAnU+j*?>?)s z1cXs^M!Wh#T)o-B4ixTA3IO3=fX^%{Ike26ZG6qV)Ijb9T`zv4bd1O?1X{R_ww+=~ zf8>GigH5ZrF(nF9cw=gjX5dslC)^RMi)Eyid*eZS1|I#{Fw z1l!G7M@D(pG-)<7_Oz?*eFV@%hrY{O9JaY5Glg**siQIMQ8%YLm<%Ug?-{#{B)0-{ zkGV7&02Fky1vZz&!2I?Sx5hfzx28V9QmJ7gGyxInL&&LNR|$tmv}V=nNB&hnP0SH6 zP&6*McfZd#;9p#4p*G*phNxr%_VuNu(!Rbqpbbm~Nz?B4-KTGB0l0j#^3X|`Vn2Y} z6b2H^o!{voCh__hrmBhr*ksQhYdQ3r*WQg6*SanR(gBYyYH@S7zKV{-%JIzP@B8q( z0DT8*;l<@LrC!I z>3nrQ=@bpV$CmH*Ur`Oz+9tV|iO0K-H8o8)Ih7pjn)Z)y26`J#v2abbea`rKwXu)t z1aHq7AWACJ=SxVu*TE>HnoSvUU7}$eU6IEU$X?z_E4XLk263DVW2`lz8EH@sDhk&c zgm4q-@i#EC_B?Yok224gmbzFm=95t(tye+o8MXw~5bW_&cOZDr0G0MiIMMy~^-haE zmmBS3?hVY=5vbX7&Ririoy4JePZ*;7Y>InctXj=utta`QZc->}rrm`a{6fMVpH+f? zvvj$}R#D^K93IU!EJH(ftm0X_VXLIpeenAKTm+T~jvtjV2sOqgeIRt~zb}8&(ZFe` zi^hXw_K@1d;T6(YW}l9t$=T{T98>(61|$Dv93!OM6_k#K_YPT;DFhs?SXMvMYJh$; zSTnMjjFvA=89A;!lc0pO(6Av7xrg{oK8{2#y$P9D6!)186oh4$Yxy4^0F;2b_z)aB zal(|u@QPFO?zhE&QVx|u;e#nFaOYU&A=tw5-pA2~E+@a+umYI&fGK89EUqPE=-wBS zC_Wj97O&{sMQ5NGs;mY^b*y7;)-eU?-gRW_8T3%Vpn^zp;}U5xkb{of@pHI8F9N*; z$3sJDgUvQ0SE(wV^808JHLGyuN7TnG*WS`wyz4%y-r=zX3QSZQAjoum*ZdxC>Wr05 zrVj2qlBn)`4WB_V*zAZLI7j+A?@5QZZ1w8(>cLaLDUsafw>Irg)OIdg?4V+uaR<3` z<@whf4p3ZJuC?h~?lkHiPNsfc__$s3l?Lm1WV$1|?^F(&ta@*8kgn|8It+duZUi#S zJ76BAWj@@yfI@A+_@1HhJPFh1wcnTN@AULx-rS*hfhkoAqS-)7x)Y9G+K*iH1)4W@ zl)fuH_8)NPiAfOV^Yj2w@Q{r=c-ZTr3?GXyhmyT_2j(Byn%r2UOE-dTe$t%h+gm&6 z+zb)?uO+cEM^8(%9Qo}&LsPf1Lmw+ztmK2p%)S9J+ri0K>Tx) zsgqRIsE)YrNr4#6R(bx-gR)a0Hz@4S+x1fc6G5#pt~dZex&Q?neWeUBug<+QG%AET zOC^}OUnvL5g$(R;Nsoq_`Rt5yxKC66u1A(3Vo3xFlQp3a9my)Sg-n@L+ulu!u3e;y zJl347U0820640oKyoi~m7DSKfj42|QFA>pK<}`ML!PdOh%I=3S29auD>C7pOCY_Pg zh;NV4_6yt44G-%)^w;h3$NJet;W1aaDj(n>?0#(LM&r4P{BQG}f*)%y*~_-5RZ37k zvh1>R;zC3tH4Dy1!6}spKZ5~I{4LzHHoq_2KCgO#!z6K=bQ;XyqxW^15=_W=j2G)Z zul$`c!2E0z^X@g#oGhQ>s9HWY)pXfmw9gXHBvWID+>VbeahNlhP8k@Yy?uf$p`07g zt0(Vg)oqU=w4+6#9YEL)F`uQ$ev^4&>asv{QsQW1V$2}Dj4_#=!6(nsV{~4W(4RXO zv@0SYRhDON1DR=W>QLtC2e>qC_qgl{GXoOImA!~xNK+fDWWUn)FL<5IS(eq&E(Ib= zZOuqdm{}kiNF6PBOR-m$)+mqQRU=estR3PA%-D2PU|@Z>Zpx;fp~BrcH*3nXXjfbQ zW^KUqbze+TaHFw8@i7-WLKZN1POvC*4=D@Pe7fFMXS28XUdN(Z@hK6b06j+tPmf4j zMh0PrJD~gK=BhvYl#*1wTMs)YfL@cRyc_u)JNqfjZP9yiBv2l)EVBkXl1Ww@>vea3 z)R`Xd^a2H>1Yvdbf>Nb2c7$51H=c=QOvT~NFs0Y2oF$6ijpW*^^8n|6E>xNd7=6B1 z$0E{#B9vL?_u?H(E~0zg?7OEH;TbSCgW&u5QE4r&xQ`@2^@>d7d5b+?bN^jr0447X zJAa{q{Ag7mO)!%~myhP5)0VOF&iXgkkzGmcwqCyQ_4G@BF}HuLA4x6CqP3GkWQg$7 zwYU#%&PO~bUDyOvcq$L%(}K^;VmIOTBclPo1;_m5J6D+5Gb0JP0nBXS`CGLuQ&$4v zU}H|4>%=W~G+)j@atarRlCd-HQ|?X#HKvIP&~&FJ0euQ|^8Px2*eN@Vx=}0OGrLO7 z`oedV|6+pw=fj%5x{Q8a%~zb+5Y2Q=&Yb=l{wErh2YInmKtS<|u$hZN?pn2}28Gts zyb>?S74x}!-r^^&FgN=o#^C+Y9@D%;nC6|jSM5~4hFEbSX1F7{w`{Solmqzm@13DC zg%Yue&@p-z4Mo#WEI)tKCdx3YF(AeuU-R|-OvaG^)4O?}Hpwi&v39kp)jN2?VZD#t zaXvJW32vgxEyf~BUY}QjMN_Aq@%K$(nAIGos7@y?8=BXy9CGzRS}cVE#@H~`1y9b0 ztn=6FnWvNAZ~X5w4NNj)lgBv&xHm^ordq;BwB{u$@&t)qQ8K;E5eT@kmaLRY;r54| z%f$IwP__s%2Mvprox%iOt@dw6SI9$Q2;G2((w$GkYgBr5sQ&F0ec@Ff(B&4z$iACe zq{-JG7Z8-eOQC!FD0)*lXeYnMY*-X$xfz+Bd4fBf!I2SoqVf8e&oN5d$_6d?I8_}rq3WUJ()c{hG1kz zKwubXaktxY4~Z=|!)i5WOsg#q9Zo~z78?-^xOW9Z5tW17=|{0IMpX9j+qL&)PzqqI zqmD)2g0HZT=Tw%B`5%O$VP0MGZGd-s;|5yubqSK}-j*Kk_=1L*GHv@{GNqJg9rWkC z3f zhy2tv8G-+~P5KogRR*KDkH ztZ?;Zxe(`T;+=FQmcP_e-*YKTLvD{2o>Kz68P)P~mk4I_F&<`u0bIJRo>)_wFuW-e z$$IHQF5|w;#%OvB^3KmWcaMdu9OaJ4Bcu3L77`~gq_Py!7=^OM;%bAfxE1RzdW({O zO^V!dHw5-_RCDfgN`zS5={1G~{B)Z9mATdx&w;qM=1f1Qz?+lyWDyGMExFfIGq_Gq zjgnhrX|+u0z(x`sYZY6)OrLVNPEg8xvMni^6Hs4KM5yn$%*5N}WWhhz=g)TSnbR)) z{ZM~haMtTm?11~eir^$4ggl)*d#sEUz35LIC?$- zxK00g*@?~}4om}p-~=wsH5f7%-Y4s#y4+D7+H>c^t0I+8Bzletw+?@OncWv=F7DI6 zIt+5>qPR=zMG5m4G2V`UxRH6i8(9l`@;^XC5 z<+tSMHOpQ5Z$!@24Bfi0Ou*9GuZ+o3MBATH3nhFHK+9_L#(}VtPuvL`-P{%&9mi}% z(1|g``K0~xwsx7#@bSI8;844stg7fZ{~@RCRFA`T{O9fG78eF|D%7goV$3Z%Y+uhY z0@r-_%%V_KS$z{UHSz^hhx~J@%R@F%aL>9s@lJ6#R$5`(8)u}gDtkepRTkYFr>C-< z%JOeNUS-bV(2*B(DwvsOSUixYN!?FEoNY_%Ft!d~$eG6_5f?_$=`y*cC#tv3+U|s{ z>a1AW6P{YF@HuY^P4qI21Hue`O9NfiF<1o8AWf)rTv#CWu-(8y zZnG&QM*&eCz013gdz;a27&(l137x*|+*v$^!~}rma31yM^UMU{cgj~x(qtMFP94ut zv%ZBV1PtdtnrM}g$V$eOWC+r@VJK-2wGJs(Y;f~?bEd&m<6X8tmNYD?i9>lq>t}2o z3*5TB3Jq_P+b;gH8b5bLu4^l`oDcQh}i06;*}p*X70#9_^4QW1AGZ<-SJUh={xVRg+uzbVIw z7jGxoC?wOA|52nSx$~n%L4euAP6~o_T*ZUvQnjT`2?u&PI2N~0GP9$EvEz>>5>Npu zShiS5+Aa?0Il%-1SNh0=?4@k$;Bpp22^XtD%a6+ zCtsK!ZN8+xqrR+Kcq8!sphw4q>dPkP1{d!hmp-$W9k~OxZF#;O>EP>_xDo;8O|Qc( z;^}D{D&9GsvcCo*#zeYCY|}CavRr!ay3yTDy-4@Q{S$}raAk_}$ArV{Mp>5LA9U{U%w1>Mgnkni>39qAAA=SruNT z;%Jtj0MJoIO{(@|_mzVqVDFdwKa^g}H@la{0z1E$r4l+xFTT@sdFlg1A*Z-wc68Is z^z>zzGTVJ3H8ME?1goeFdE~Opvf*i`g0D3b_G1BxIUwdRB&5WU)G4Ma4w05h#QoCQ zOndnwXE>LTF0KY!khfJccV$76;Hk2oXno#DU)34BX5yhMao;H;r@HS}Ed)_uFCtV% z*%STR{JV`h#aDB6!tx*wCdQo5<^d$29$2!3Z8(a%ooC0vHp+e)f4#dOD9>Mjnj<_J z0|Lkd4r|Q5bqdA}zNO4mXuV`X9Soqjx~c^%XTvhyMff@Ajz*FU8z?x0K{akPuC4V| zd-`zmwTH9EuYK|)Sj6PA8l79YY!YRDDzA$8JVK4O20`41Q_cg+o;Cq9(|NcIBz+o_ z2Yl{?gY9T-Ub(015=Y6ST~quL633bI=>8U;>UhfB_Xcs68Ke0cH}~$N;XAZnl_50g zSrdD9C&CNsPVrdnNCScg%qy-Oy^LWn9ISS=c#`04+F*XbX{tN`Is5x~$ zw1wRtB#?!~@s{h|_{g1Yy%fw22q$QyWcFIv&J>4P^>}}sZ&o7P7mLFzmh22{G!u4y zSlxhAyoYJgZ}fcsQjawoLZ81;WOUkA%|{n$+z(1K}(|Ky1b?vuBZt`>KU15P{o#d?ltuPVu6me_zsP|OwewdKl@myT?khXSvWst; z!+JjLV5{}7d~GArAr`=N5U10 z{6*@Nc*(8~dz4<(F(%pFu4Np?RGJT#vj`8b8`78K=|bb#Eql%FO9uk7m7WYxW6onn zI!x!)Cj>JoXp$)}J9_SeGrVWDiVL1GshRN_j!R2*QHQ+J?`5N|k|d@?QA$$PW@8}%tl&Z9 zSb1B}xmiwg&uIFXQ<)vRbRf6F6+Izkfi+z_PiR+Cd{Z z$>})7-e|DF->E9(pSq>D?;q&^@SMKzoRiXjUIaBHR~KNGngN}&6_8w`%ER7d7GIOh z`fgfG5f+*`j)GU*d_;BCdq~|Km?o7scV0A2vBP*jWO?}IgeXbbxy4y*HIwVl7F}MN zDSM~27fIq$Je%x^wzcp}RO*KmG{rfJxCw<6O1A66Nx zx|W+M3Z6TJ$Ogj}kI2=NAe9jE@j-p(R$|9X@CgUY&ec4`kDR_;3Y3p+FI+q zkv8Q(@ot_)4Z%<4S&`a+c5ec4HW)j<%qu~qExhNdDw?}D!me%hBxU_^BHF{4egRxS z+f;WfnMj{lZYJfozhV4wwp3LqxR$632gorWbb z%E{c`dslR~hx&d3-9BF33Ya_i?U2WnFN9=eR}Zca(FY}yQ*`a-jmu>%&qIRm_7>hI zV2qB;tYF1^9TyvA^uU2NKTncX!%fHs60mHaMB&RxjHs zhnc>1S?ZfNxobe(@ZlK6xB2-*i?)Cs{wR?NJtxElId{qq3b&<7K)9XE7iLAtNcSX| zC|ya$-SPX#2Ycp)Tz0w#BuJ}#F?$oQBhDL{JxG4Ox9?dHNIT@cT4zCP!f&1uMErKL=vlMA1C?a^cv~24^%ib>qaj=A;vYXxt1*Ka~#|fGi(?&9~yozHgVP zcD`R5PrXivfcJbT`c^(7EMT3x_%gEd2z!AmE+ed;)TeAI^yGtUbGq6$qE( zAo(cr4_Fiq^&{&Lv%=pm?y6`mu05#tT7#GbDr;u3b%KY!scI!-@;A?`a>KgNN-WF1 z|7}I4Ax1dtbv^b=ebMT*e!R1;ce8P;<-SE@Xud3==ZVTz1I0efh|n=5uf5TaV$^ac1c% z6Au-fax`ym9^s(&?MmhjzwK4>O>6RC1r}a=NZ#+L5GFmZ$(hMtm`zwGb!Kq}eN%A; zTfb1`Xz-DkH8c0%{XK$M$AuUe`vPMiN+1nSQdKgCJzPW)@7>IoKAjIhsY=F3=^JqwL!MW}J{*cFXUp7h6CD z->KyBn_FAW!`|dEa)S2u@*&Ov<770J!Az;Z!u}|6{JF(yO0*vKXA24S8^XuGt-EI> z=%CXxHllD|)**6W!dAF_zJ1R$w|wmDq-;phQ(fk+IE|Z%YY`Ma|2Yg-PqXyx9$n6B zc1Ls60yZ-L%0Ij|t^1X&+sCsGy>6JlJo+8$KfebD{Rod3iXmR{J^W)i1s*(ntW%`} z-rS|pIUn7d7z1@;=-p+LpiJCUy>~*~iu1oHHfG`H%wPym8hg0ck9x~U*F(p5_wy_& z@LH5DK?PEsSxdM;f!%{dVu$|Ds@mEF3W+(gW?7fF;?5nvk+5|Bxh(#}|FkL~dAct2 zMe@zMB;n%4L37o(LoUvY!qK~=ps8PVj0Gki2_O%k#>NIwljhMy4N;u_P#Bu?Sx zZb7;8`3tem#Mr^z-)=cA1(uLn2*2iv*_nqf~86iCk#f3d0j=gr%o+B+kON; zMIn{vr}T7{`i^UAIvj<~ed=gN%qNZ<&-Wf$JXEZad5$t1btn~0&NpHV(m9FrfWk~& zq@lq?u}`#`7A$>r$*@0P zawL*@6>Fc_K8Qkg#oe2v}{d+Ypi@$d*z2RqVq_}bbzzMaurTsb>BJNFTZ>5CUC|uO zHC1w@$uj0$ne_%un*Ur;QAg2}<)?>}Z@$8LO<=W^;S&`&2$RQwbG=^B)|& z*VrBcjUu+UKPS>AV;3>pug(&BD;Xa|&!(TB^4E*?GA)KEmq>+B{4K8=Z{W{MbfPfl ze;NeKdz;Qma?<;3hY2swon!XVg{*w$DDS5lU%x(-+oIB+vxiw?B4rU~kMjTaeFuu@ z_-^q~8VupuL+ty@s``tE*#g$Vd-R4L%>Fx(tu*7|ZQh>bE9X!BSTL82Q{Lr&fN#VT zjPb0{bCu$Im#D<_58YA8)}!AC3`YdUmI<*M$04VjTerm~6Ky?|@q%N2`S|}>il;h( z!Ih9tw7b>KSw#@}p^z3x+tTS@quRxT^0zWDfK^1_QKQ|jhBD+EnR6DuR_l-3SzGf? z3%a@4B)UMj>Z`E6ab#Z@m6m{xtj(ZZ{`u-Kp<_jiTTk}<#!B7Sis$p_s0o)B$$AJ0 z?G2N(Fo(w@Rn;~A`UHM{*{_QQnU+|YUVBXTB(0#xyqAcI6Z_BC1s*(V88Llu^E9<; zCrCD*O5FZy^ZuBJnm1Yo1H}0oBPrB&ug!`ih0p%&_P-Fy}qdX_B9%t zAvASvl(^>;eY*JPQaHxoF@St{;Im+E?&<+*E;@v^_Fp&r_#5upKr(XFl)jy4^uEV_2T$$888r;lsyn1TI~saT53!cSroj${$NwTeo*al&@#L zt^E^CgFQ6y4Z`=IsRLp0ji^uId+&HcSL9BM?Jing-cZE2%)cNazFleN&_C~O!_m_# zH+Q%AHkR;=y7+)jmDKz>dFKsM48IV&x?-~k){&4?p_I{UYtPGVP5wGLFH%p;X}2w> zAOGEoetI~Nin_gc8=K`IAMy&?uaOkcp7bAYW0wpM`_YSVcD{I(whYuzMug^252y$fHyL+iQ3ajPp}EE9N+MHYz_!E=>sV*%YbUzm0Y(Y1AthHytkLr zcmBp*Y2oKF&yhI6LmQ05=Xg&Z{IL^vz0sniKuWT=D)2OK=mJ_16DM5O$iDZ{Db$~s^?c_oYG;o0 zvh}V~U~WR%*n7>etH93tm0E0u#VKa>WBcx^%m1(tU-{0um^>P4_gfn$_!fMrEF-&L zA^3SD!tjgun9-R0GG>u~$Ry?XS=1T&Qg3y(tJ>yUYBv+E7uPIiHUBPd@)15F)0^!& zGS%hREYU;-A%6Uj`RDl``zX3VYi*R_U5gy0VDdM@E+3=4@M5z%^wdmEzLtN-Zdm(R zE@N}eJHDRUIo`@&yYJ^N+H8{Yx(a)_Ysv)9PkL8kUZ-*U%Hbb>4VKy(cqh9BJLl$l z`GzR8c4M!o=ZDB(EuoUy;#-iLLV3{o@od_nMWo{|pX#T5*HYWKL%U9Rz)!GTzV0-f zWsu|NNk2;->7>pK51RZif|txC&ooG1M)5Ru=00euVY|Aw=U-x<#}1V4%+!F-J?c=bVZu!Z;W+|9M9t3k^Og2cO2y|8}wk$vCB+CpV(u~gP5WSOxqGscoEX9(HN zF!tSyEMqX1@0sr$bt>nl|EA14@B2Q_{oKoSFBjNbB(ZS111xiy9kYi|N$N!IxNR$x z-*yA(iia_QL&Yb0vY*mFRhXw|{5U%z0d+13|IMal4l<<*r6@A5ak-`pA*`ha9BdgRcHOLVaK{4{a_gV^0`OrlyR=7b#ke{k%ziS z{K5Rz)fwaTJ15SzW*WFSxW4yYxEx<;`e}a zRBJy(+j*!b=Kcapx3F{jf86HV3;L=Nv;%$Yhe_a95Rz9b;ux(~b< zCJ_yZENYM7FG|6V-&N$+yu9(Re_Nbw+#(}(lFuozb`W)Z;36R~B({G(7Q5BB9{w%x z`1bXGFIA%eblC0yWQFJ5ku*Bi_^mL{h_dyK121Eh>!ZVtsCDh zH<5k~3&Yu*0*=N%hW z8HYgk84_MeazDO(l3&FOk+oOe!zG_1SZ#yT`MuF!VGDCCoJPNe-o&0q0#v)g1X&5vv86^;a9FSlZND<~G& z*y4hcN1Mxt4P#7(Kw-MQEif;~9@;wlZ@wsSM$=wm_o*Py1}n0zI|AY7X0XY#2?1Zx zVK=p9A5x~F56(25l_nJ`Sf|Uc{M{aHur5_aN)>$Mp%*SPy9Fh!1V>7}L)A99rh+SF z*D=B5$ks{zT|<*#w0`n}SK(COx`+Iqdwqh;ge!w6>OD&zc{9LUi+@KNAj^>b{(Ydn zZqc_7H*}cB{SdmHaMelOM)+@EhHVA8%y~9&|Gr%UYA>dbJl#qS2?=R|EgvoL_r>0H z_JrgmZ*mm_qcCTUAHvmD_cPom1H+xe6X?OB_(8^AZsT+EO(X5Mr7HrJLO56$DTIyg z7X@N$$LzPif3q*&J*D=Z(Yi&CXIbrOoq>HrwQVr}FS0(UCDlHD*uDQ;#(#bxXsciS zVcpP~`Qn;}=Cf!|r@?gWn^S^=o2wFAx+?h~G~QXO$JWlPyfA-eW^Mz?_-~5*m!D28GNS?s#Oqe^(_nylpc2LQX)oDH~3tH}BeO+A6qiG@k z<8H>Jco`U-{rC!@9EdqpKZL-=67J%_?g>4;|GvfF zud4(T+D`uT#v_Z1Kd+vzN=8siv2$Z=>|eiR4)s&uA_woGL}Rm-1-@U* zc(4krRF77-9#(Vy`yc-Kp7Ak2 zt6|;xJyZ~RSH&^BqCJEil{uTp!xq2F9W7g_qJ|Cbh~*SHZdYd7rY;{ZE4U)o3;=fy zX~Ln$B;BX7vc3XD|5hviR#xhM3P7O^oSkupv=`5AlsW(YjSey`F_E=f;3|_A1m3sl zo4&?2`tLuOyf?q?P}-&|^-w9U44Vk=f$N}0WV;5$IXv0qVVhjlSjFhYNG)+JR0Hwq z$)2jK0x_E@k<%=5JGu}Fl+;w9clyq4{#+S!;m5GHvjx_)J5~|3c`uaQG&a2E?zWlH zZ4}npMy@m>>GC7$iBYQ0Q1f%qw>O9YzklLfjMDBd;j5_jEGSU^#gSaZ>@H zpPb{ul!S5YBx>)%lI-SZjX*~Rcy>}O=0&|w5!RYa-*5K6PT0Ceo#RT`PNsNb;f-+K zt0f)UQ{#vdflZeof{{{&-Py{UNC6OE$K)hd2>}-}AUb&_T8Z-yX9hPBC#87p_?+l?ENhFf z5sG=D{Sza%3YI~tTn7O`!R#biR_Pj4z6B3evj z(|EX;MFZ`yWdngyzob4;)V8T)5Lh{ZY8jJqMdUhcqn@;u^l&g!%eqef6!5QP!uLY) z?V*9(tG$K$lT>oywQcfpsh@|_PQ-F=`hr?J26ce(Obo$B%|B|J4I)?C&A7i@Out65 zHWO{Q@?Rpd{@5N&Gkj#z8i5#&Y_)L2Vosu=DS|zG)5iq5zphVU+G8>95x({jcHzJ) zOfgmsM{j5QpHFMPN5nr&a@$i$|IkqG+Eg^Q!~AUBw;bbtUEV126rdDOKq5i%Nq6E0 zEGs_o8okQ=zrO$XpA2>kmO(-VdtRcbNE_721wClvX3-rJUMrrvaDFN9sjSg~?bw^xX+I6H_i!!scQ#=7}Xn%!o`fPwKHD1D%|WKmV`?(9KK<@^kn z{vb2>>q6|uj0I1Y)DRu9h-bO&H2n4I&0{WauD)(E-4$X(Cwf!0 z9>wYuaR_l-zag9?{rIrcFW_Zu^vb`y8<5|vY!aQ$Jea5L+Cr-}%U8J{u_%}MLB6NlDX)i}6530(7I=t0tvsL%Y=K*N8q$u8U z?bs!J;6W-2!SNNQZ;-IMk8#rz$~t&x$YlTGf@UsDPg*ZlF7e>45`6x8!stJrlOzFz zBkw6h5^+}U$>(9q1I3?*5c->Y3*t|&QL@ok;q4OuFS`YGr6w4>lRviUL5nX?%J=|5 zn-J%3aiGkro=NgAZ~BPG>6wERE8C7|Ky&4rbawREJ<{FmPL1=7kbQY-cM;DrBP+n9 zYlWI3Z5s$#pZ8b(ntlW}5j8Zff)`lyP$NHD0jLG&p-oWU+`?JVH@+C2M5?s7 z(rt;UWu-2>-|W0HFhb}m#eI2UDRTy-B+SUBrL0r|am&pXG=495&Z+umh*%`Hjlf?~ zH*#nwF;V|oKiqxuOo;f?fH2$oW=A}Ha`L^M85rNSiMX6*1G{IMN#v+C!ohWlq1O@i zR&eVM8-?yo9a(ctzl0B1y&i$u!*@fiAk>y66Bv;gE4W*8GlQ_g)&jDu%C}UhplA{x z*4#Mg_RZhz3_Uj+rQQ+f3rU=F8txk=@ZmP7uK4~K`>FcX7i<4E38d3|@Fh0?8IMXR zD^>O6rs$5)I}m1W^VgRgwnr^_xKPri`K&jG7BiHj-g@4i+~wzCT{4fqOg?hCf}9^d z)4S=WpBI7_wg&`BewcUIzlh&6yI7WxJZ^RN^QKN4ut7$BaE3qMAvC2B;n_S8NKEUS zh)wTD*GtW6N$_IaBkWO_WI68P@Ar5k z5m$LgFLWz|50mox26;csXHQ}ys5w8gxCO(k8$>nb!!Juezf-@!of8 zEN4^L4R06^{K|AI)c#u{izgA1ksPJf@iUvo211R(KuG{hA3#^E?Xz{Hq8u~hhP#`G zKL|FEd=t1<;+w+S8EA@h3cc9v=*m?+qM^8vgY)m#1{xb=&m(VoAgv_zX7#3pfvk@c zYlZPd*3HCiPYV4~9^J{jC-Yr~x|nL!<9AX1uTXK{$`1ZZ$K@8$_mbswSf*>c|FZ^( zJRwlT{tK?40DT{C0qFfbb#RK`U4Jpf#5;<{{9ni?&zLpFV=o;0q=1lM4@6};s`@^Q)dZ|-WD`Hn8oOmVmT9`m9@T6(s+ zfi0GzI?<$DQV$5MjTO!Cxp(h^DNz(sR^be2CQk-rzNOxlQC8U($H#GNXyYlvgJOUf z=Q1GhbzI2+q!`bV0JT96953cLTz7G8m0CJsi&-3w;#n(gXj)sdt7@=l#3xJm3kmF8lc`HugXi$6K*_h zOn7p9<$1G@h?vp<^FmBl6ckp*G?Pt2WRps;Z}sZW&DJ?lyIn_Wr!p&ck2#|dF!)+I zyZ6)i9-p-(LOm$*0;OCd&@KT7okR0kysleR(0Ii!65hVyaN#3VEBJc`Rt0*o? z(?S2phTjmZH|SLTh2&@l^%bdA3d-l*t}`9+CDsqi-1eOviFRyig&M~QPVi5;sX^=n zgGYiSQRhJ2A?1>FRwXyZS?p&SBOM1B2bLBd-Eb1W=_paZmDtT$S|IZ>dA-0#oSo6N z+Zozyt(rIcoV821t`v;kX>x9dMXoNjmnQvx8y1$*b}V(g0JJaVuFMl;x@*mCj9fZR zkChmmXmaU)d^8Dk=-9lCv|l~J1ZHPV9g3q6sU~(O2_~+ zLB`%E#~viVi~boSI8hG(F5-SuCpHq%G#NO|pNAT4og%OknX zSLCHB!A=8N)dH6|bkurR=9)ESU}T=s1r;y(d!KXa<-|Z+w;rYBv%%v`z%3>zFg*ll zm>M~ZP3(CJ0R==;-K$ePt{uL#wE@LdVVKirm8}A&UdD{XxWbqEc}mZ|RHi|OXXDm9 z@h${wTDM{cf5qa;uA^@SCs)KJt3g9M*RfSZcL#j>3G{8bUXNPqDl?B!qniibZ6LTg z7D4W8;o&u=;oF-)M`u%VscW?;Xac48)XW8$of8J54#j+A-%zDi00Zpe&={CLbr-*a zgO;4ME?FGb@H+F8PBqa0%7p`|RzieD6h&S~)OQ5p=Zj6!yYp#ZV3zYQts=V|ZvKKv3g-(k{R4JN`=^4qLk-Sdcp9a%hs+)mOFUN07{~3uC6_j;sxWpe+k1K{yH)S*4q;O5j>^rXxR$b>`POK9@^{rQ1>ZA zX7bTEPX{zO)KMX@E>$x#v)#3!^5nwY9&g&a9s7RY>89>E-R~GjGisIsa~by($~^A) zQU8JAOi-kGXzBJ!v3t~8MJe9Nm38Dwm%1E)R=5-JqLK30bAxoa+MuL+l#+^DK!oGIv`>V9HW?9HB@spZt)8o_zdhk ze(aHONJ^XxUQ9cScS@>-Te(+qjN8vdXJvO@W#mC_38S+u%@I)p18fHZEem?Q5qO`q z6(B1wI|Oz6wIM{Z_2&c?(E7h@CikUmYxA+>=;v!R!L^|M?8jF-xlt*&Q0|G(@TI26 z;UU1x7~P&m>Kgkok$u938T~qYj+`Bh6+o#i!E+t-sP>k<e$0isk)R&=%Uqc&A(e ziEJ|9A}m(mqik3B;+G3po!^2n7TI!6I*|E}8Nt$sb9VQWj4AUwRsg*w4Z?QhC)U|?E z8zwC;cALd{HHCW5C304LltdlBN1=bF_+qJ!I^_swqscWegeY2J=2_^RLd%&SOyP+T zdJXslyN>F_bPk_-Ns6^(lPee5S!i9G`AHo+fta7Z(3m7<)SR(B252#6LH2#Y>n>A!Kt|eW5FKC?cQDmP~ zQ-#~zX_eQ2RZj*|55{*jX~=Ykt-W#tQz*}n*b27pJ}ac@npH11ahk+C!K>>FXC7BB z>R}m8+|eD7Fs%R0Xwj8frsodeLnl7PrkjH5#GM^!cef`RKcvl2E;$$ct?ZO&EayPE z@>rK+!zahY9-xdV+v)Y?jvWI0#z3zAUp*f_9fa!|2vt;IrwGixryC`TgR(k5{vG zRW9p_2CT6s^5U7!+b=lZ(A@5F{8pW^E5qO=n1?ksrYyLm)oi|>Ub8JQeRw#|7;f-j zQWDWwtLlWg$%R@NNxnI(_UG8`?_c)B?{qvTW3RJ9r~_H%pkls6Sqv^Ju+=Kch_bBa${U<#0- zQU!NTw^G@Ovq2>k6bpnH|z% z)=n3gWt9+%YA}*dFC%39q-g#C+u>l6nIi4_!8)ylU|?H3!0ZB#9vU(7*6YWv#RL2r zq285T(9_nTL&2X}2)uEU_YiLoi9a@molsx`1px_2FYTSe#^=uo54^|&#ZCv^?)J`&T+X1wsha}}_g&aPGoN(H%?aINHknNn5VTak*huNtiI zK9p7{TUXSdc-H~&-9MTmRkw+8FwkU57eAE?C`=CYms(;8V9Zp85f%c9=7|_985TZ| z(Km~1O4IKirdH1JxDwsXp7_9nDQC1y5jv^Wk>Lr_dKpmVYN+yGRBzQc2wM)q_a4WM zMS75KIZ$0)?7@*$-B6W+H8NfmY}>aU{X{_hghS3A_5v!!%AMAntf@KWHHEmw*K9!^ zt$Gn6H=dFU#5MVarh;sY&+4N4qs0!vL*CMZh`P1aK;z}Zr0iOW;N+wf18XZVv(vhZ z&kpPQ@Qb=w#_8VaitYotYt2a-yfb59h@&$eY_Pv)?7pgkvVEH8o?Ve03R7Tr2ih$O z_zuTG3xvJb{MabRBcBK>3@P6oPK_mom7Y7@21+K*+7?e)K<>@2&dbbGdC+P;CKrrh z?LQWb>nkujJVcC>MN;vB`q+j07vOpgd*6}2Mqp(pdCpFM+}GR=rbL|fSzTVjS{3&B zA7h-`pY0B~%_yst8CJk;12X{@RCfp6y0A1`FE6Q*sh-1RpDIVF_X;SFRWG+G-wiH}dJ|o6PJlNK z1G-#XvI~#)rWL!UA)7Hi#kLL6vN;XH3d>(Ayq2o2sy!H9c{qfvn73ehpnt}n)asEo z@iA+9FFN#H4;t~+5&7G`SMZ3`hFO$*=oCi2BdMrO`xJeFjVgG;%+^ajR4?8DuH_}c z<=*Yr>M8}e#;2e3dj4h&OwkqNMI3bI#4N|^^3>X@Ez&CV<_OJ_BMjIJdu2>0|ry~s0o))*RR4K~v2E>HU9vm?bRl=59s+5*t!&85#xy)Rqt9kh+(O%lp z>7LI(RVRd%Xt}jk(kFB%WIcs3pee*iJ@WbR7N6EMc(`#OW)nO!wGVbG(U{_!aoIiY?7 z&^_M;I?!Ut;Uk#!7n5K>d$8|H4eIPXrf0>*V>YTMTJ7;8R=14nz4QhnpXC9;DEYvQ zh3p1>YmNl=mQ)<&>>c~+-JB`aMbigIU;2?Net0#d8j$C*Nf_`SWzB=+7l6yUVv6 zYg|uts^)hc%n^3-DQAN}%?D9;E_nJRdM8q4G)cYn8kJi?x!}y0!I*#tKC+OXQg7r;&!ZI7KxuWy|)-8@!3c-2S9sy&$clx+#h{m(;&jI!;@=;Orcp zn5X$?j7)LO)g5a_HsGYG% zu=G_X6>Tj1(r`!*ucmE+NGm(aDzVE>ouH; zZf^~^TndIYHyMIav~$4Bo>Z(j6+0a?esaK}2*fqi#ari>sYh24EyjUkM-rNRiu2ih z7AiE9KT>6UXl`EK4L^W2cwM2$>tPi&lA_rZ3s5p{W}AXb;VLaidp7d5#(nh2o}2yD z5-RrGZG`6`q0~+uas9@Ll&~`$H|{$PK2u9d43V>F5q*}}FFWiXVu5#43A8&yEcKxG zv5kSS!U`SNtf>h3^y<{nN-e5XowK+Z7zIlP`?c%=oTl00?$$__*H`khbeQ_7+^F(= zI;Qc_51~^x1-_%9TvJU$m98$y>h#=xpi@1|lsjH`ci>ocvG(EylSHp3mh zF-4XT?Y5-8SaUhhN~%mt95>3Rj(4x?KD)i#)KJ2KSEHb$=?q^U}&4*$^Cw`Ct zurJOTsCArj9STeY-WsEj?j`jAC>2}9K}VC_)XF^Kp_TrS0-r%f`s6F+>gsSkBl}9u zxT1?MewW7fJ8~X!Dek|-*J=rTA;IZgbuXn78zNP1c|1tD0Z_1bZmN4ZGtZ!;1s_oU zMQB=7&+?roH%GuQAJx1^pO1fVD$duti5Z`tULwpJeedbSqm-Z!t%h>YDxp0Fx8%rW zBZ%!%3e_n<4u! zXLV3Q8GX&4KzWzm^;6ekraX@V|Hb84YJRBf@XNV3>|P?<*Y0Btb{;7O6SCjYJ%MAQ zJ$V9t3l4QNocYy!lgjVU30~|Fn0ohPYO1yr6g(@WcGenUF)RdmkapoigG5iB(W+qk zSI3M+e2P(9moIz`@Ta=I23?}aCn1feQxi>)BrtBgsZ~t$P2zJjRV+11Em7UWU51~X z>a)-nkPL><$|&L-V`rn6gfjr@au>jFDYn&SNv?q=-e2wuzI_?xRcL(hal_CIr#SV@ z{&5%2q8k7~)JU!z-3MqhmAz5}Iu?r>yd*TmII0G)l_zFI3Dq>)&aX{(e@DdsUx6K3 z2uZ~Y>!a>b_4Q@}hMYsqSf2$TMx@1}0@h*H8#`G(YK?+vzoxN)%YNGhY8<1hPx`yY zov}7};{HyUFA%!38Vh3Lo$v1zl&X0yPgh^7^MmE>pF!AHG(xLGZo312KWK1nXsL@O z+!i1*%TEq90GO;@+b8zrVY-2((7T8uZiUbmE>~uw1Hk-lDa|8vfGP=0VRdVnG7;f9 zx6pgvP(UD#;&vRVqSb}@6sK4!AW(g7aJn)itEu1Hqu?PM&pdhDl?EWO@f=jEG>uPZ zLf?HToVM%=N$u=e9+pX-ZBQ)rHrTJaXXTxw*L$K&1oYOQ&)(=1hQ3wza^H`=Exv~A z`k|-}bR{pMPIIKYs?OGM%j^56nqhU^fi}zb26V zR&ptE!7P=DRr4H4h3<5OGk0eNohY^dxbRvE6M32zN_%v3$wan=JN#23S7G(Y81P8=$B*w^ zTA;&t?x!D=NAGFgsY7-6$f5RD(zB24zO40oz8nBGQrzPJij=5n?8C49hKR{S-(a>{L53x*)E49U?8sfTShc-v>@2TetuJ}=HFN{*L0~_ zmDl=ox@BG-?1JYrl)INp4GOt31`X7>6CYR#*0y0?$wNJgY zCG4jp?$|dRkHn1NsaekLWdDB-2B>siTZ~(KbwzIM?eBgs1!YB#ul+xfg0Azb*YYIV z_ulFA+D{JO+w)Q4FgGU~chKhrXeRHz^Csnc*-#uNkEN?pw6(QA>p#LC&KD6$$hyTQ zs()Tfhx6V06H|*2*ZOYxoKu*b@P~`Bx9^Z41wMCNZc~dI968Iz@Of#Lg!~N1Y2+Nx zFeAgCqRc|UL8)N8$U2wWl4G#nb+LTApc5dQ@T&6W^|;ejXg}?Kx0o`XtF;Ycm1so@ z&!7_k(a}2>VZW0&Fu1$c1M?9~XDkF{7-+o=gHFq{`Pn6u(XOH#Q*p~KhhVQOX-2(| zkH$Vq+t>0il98U1x(f=AoMyrMYbS$ZL$qvOkH`JUTWPSuCf6fIn#%n zia}R+;pz&l+AG+(A4uAscu%VkT)k0VU=*3sYi@pjJG+a|>TFy$ z$eauS%tFT%M5_41pTxu{E7Ts>)&teDlLbh)^fo7xyTJD{^%QlMmRJGkx8*=W0KeFc zRQvKMX-n=>B(RhAzJ}**n2QMowo`IWqrJK568J|P!jq@=&A*#%qzMSH=0`?E_*d>) zN^Q6SBZ>nk8(PRFS)6$ub*R03Jay2!qu|~=1TpUff@wC`mAFn2m^Q4CY<&W04jNqm z-zLYn693YFJM|nbatsxN1cNng7QXb60p5x`TXn!Jx({qi7-*0-a~gOq(6D&uRI0_H z4kRU4;}8S&_S2Hol^njGsLl?SDskFT4>_+GpLfNI5MUan!`Hl@&&*Vn?^|cO$^@(r z#k2K6j*ZLoDCsk_<}#o`>gvccIX-Y^^4gIkV+2XM9c5hlhf7uyDpAN4Rsce8lry&W zAW1Wvea<{610B9A6}>H3vo!k*b1o1>>fDzbpLOBDnRQVPzDHh?)jYF{|EZ)9YIT8Q^8YrE?4dPy+LuU7%}%*ts8i#G3PQ=WntzKn0qsSt<5{tFdbjwOOn2&|4;Uk}V_XT=1#Tt3?*mAk zX4RdXJD+eb&9*Tx9YpzhuhQ{!)}tv=ciRkYA?*dW#;&7p0?@!i#L=CEE6SQiAb(+h zmzLT0^pJAbp#Z+UcP0jPxei=8H~o>7c2L&gc8uPM6gywJajiWoon~4r&;)RQ|9$2`pU0ouEY3_H8+{_cIb0_b0_I$(#QTUw+m%bsX;IPp<47i%&ql) zxG^ka3D92ocOJxAbj{zMXnJ?aA>uC69`ixQgI8~XSRQE?0ibs~RLN2*27t6(0Eg~} zdrqs!*b<9bC!D8<-5|Dc=31Ia7yzM}>p|{AojSNxx&!YbjUW5t9+@?lo$qX;z-wgs zF+h=GwSb$iKKb+G@-dpK--P;OaMfaoUe_&NDdspmf?c0_XV7u>N<@y*`g=WrDP2zy zZRW!Rbu%eZG`LsBsw1=5q%gqw1>5x$1N}C^J;g&2hE=oDuNwC^0j%J< z#+gq=!S|dy88FdeO^=lzY~lvFHDe=5$k*!K?JC!ONCSXi!2VzwT$y`$VIY(_CL#Zt zlF>kFd!_rJoH-%ix|?q3Rf7+PXXy{&19tO|>%2?Ml3}i)V6t&#JkQHxCp>c5VW-OH zjpXJIePxyt`kP0upRHp*ncIr$=gLgrateK&FzJm?E}@;Ha;>QsF|M!2jU#)s%2dw_ zGW*Qg76Kzoq$fXtUlW?`AW?A?^mKn!2vKWxc%32 z{oBBU$-msu)YMdn`h6doAzmWto({V0buJ*SZoQpTOfV0k0W?H$%7Ht*ixKFmoaRL2 z>yyoz0GE{OgaN7TdSw?sICVO8d<(xhr;vt!sewH3`00 z#z>ZXbHWBwNszX~Xe=mUaG&{?Kb3-pmYjkWYm4Zr?srMalF^}6kmJ&m749!8j>$U`opjc?FVI+iPRBMRu{Iui|evmL@2 zS>NuvjC%f<>&M3yImoaERW{86Oj;*{yUEH^ujjALZzq`BxkgDIfdR&37ZxVobzRnIIkJ@6mV zTmSY(|185v_HHFBWZRTeMsHn8C?~80$$7cEukC%<5B1)O+_S!lUlrDm+W=^~|6uM1 z%!;YVvzSi{CoqRe-h9-v6WB&Z* zfkyEO7MM3FCd>b27kc;Q|9I1{i&4gu{cpla9kKg z)VRn*I}IbUl=PoD(h;Tc9i+7rAyAvniS94_+D1LOyUQ*|$Y zg%-N(-KnndZaDl5k#RDjFSaw3=U2`V%b$$gcA+N;iMzui+l%j!wM&O&Re+ zXK{u=aGwB0?G@=}j3~8=^;aZ>^RX`=b;ebyy`MbP3_-7&^SPRq|Js3yH!7>LlZ?|n zu;}ClGbyAo<7|>sJ+D3je&!Erv%x>3XBnt-FG+WU&z8l`tSB@gyXHmacK_NQGcnYH zJ?>0IYAFz()QHt~Oa4{?v73Ife%^jmasSdjO26G;aAiX#=X6%=Cm$2EDlkWIx>vLP zk`CC*Xz!1yvVs1NsIe<3c46+$2<3CV-@D@%F80gyW$xm-+ulKH`%p$Y!%KM=>HBL` z*moHO?WnROg=Y|)sV4LpmBYof7^72T_lwW`oKc}Sw*D6MDeMxb8CCL31cK8xR4y;* z*KYp%p3pvlt6kFm2u`pV!@N&~fc;a`g4;>c466qDwUa-`c%8vD zJd9nq01@RB4S}R0KASN7OK$#Cw3POU=xRZJ%du2{MS86{oSWR0ovDEOm6Ipb%dDPp zyI7>31#$88`$eZOF*{uuKL5he*(1V}`Fw_3OLKcWmpjxS!IE8Vzc6F{@>iA@D0T5( zvYJgn`uq%3&_(hXdtRg_7nWzsuRQTfAjHyK*?5XYun6L5FW0XKJ}aYr8SGNRegvn3 zLM$xreO!>}LK zYb|*LOOs`O?}Y3sXInQOnT$KcVdxg@bQP}+y=d0E4P+^tLF8oO^}(jOWZ>&eoL05#yhUX%Zn;T-hPY%_BF6up9W z|3jb~?d<3gYKaH)+E3Cv!GBBV2ZEB11}u0Io4L`?NWh9J-EIAj0HJm{lKTP#T>!6@ zKf!bMlNb!zz6dg3`o8eES$b%D9E9I_R-H`>#+Dg*5fg+0l&lU~nEUWXXQ@#MXean{ ziQfdd!U_iNBZ=KwbbS7cnrG&UdObJ{g;6OJZ>m0aU^{*!X~84c*jql}hMY zJu@L12$_~}6@4OCyph@)Mn$aF}^$HaPAaWLEz z9(iCD3`>2$Z_*fl{$cDLB848=yMk&q*_Z*q@o;AZ)c~i>le)n)$S%M$(G4m;2I+iX z2rxcvf8XsZ%0FBDJxVv+Sr>c}I9!dPt~+y2^B8Hqy67n-)*BXk6qmK`Gx*)&Y%KjG zQL=C=xt9!Z$%$<{_wU)R9iHX2I1~v`6nf?IvoSc29&0ZV6tr=7rQa=sF(8;DW`VWJ z)b;mv-bzMi^@6u?#>GZx10*539(te&x0mxuDzE6EHo=z`cI;Yos&EYj6inF3QEw0V+Yxat&_;U7(G}ER zhsXow_JJsk4|2sDOW9M#d1%=l&Gs}6+EKRRK0=m+s59l~dgtZR<67eH|#5vY}-_a{hq7)xcHlv#tI8?F>j{li7 z?L<{w13diI)Yk30ol=?NXBL)r0<4&E`34 zzj6$JE~wyH$c&gJq0~YJ^oVO9ag-c&&OO`ot{HS&A%N(tc2YcP7v=@+${1__&Ag_| zP_*6+7{lsT_H&ISER~oaQm=A-(YACYpEjBpdT{0C$ie0kkRfI^8z=xd1>`lt#->m| z7I4^)uyXKoVHNoIhIS2Bkvuo*0o0b0#jIDG0^WAkZY`j`K(ZEK#M@Z)nQQk zoN}V2`kD z<(-3a53i(+zqr>%ho(+<2+SwX%1&lhi1#9`Xp_De+`FUqKe9d%V}l(T);$byhaBWB zd76~Hgun7`Qn|SpR}5HWF=RT#8|jO&nmh10>vYUHs-3{Y5;CZ`xgsNMZvNnAh5z+! zh!SgE3J^D&%*sw(!5M31#dQ(=Zd)lwfD`+$HE|0%3iZmSL7g=V%y~)9te%e$ckcshy6D!T-!%x{fQnXD zM)$yik%{w&k!<%O*zFD-pqFNmN%jM+xi;2}WKiChcao}E>?ZliMCI}&Ru|E_+t_!*XotcvNV20SAbhw|HeS`#2q$R zD6tus!cJ!rUb;tOY{RfD{yY=d7+()gi_j#e+iO$>OAy$+9ysvTILE(X=Cq@?XjrE^ zF!PRptQUhCv#nvi)!p)%1_fGreNLCIDomrZmbD~6dF2=i7cdFqs zGFB>M(kA$tqWAJbp)o_7=W(X%ia;GML}$0hD0rcnXDVdPBqg2T=e@!wpnA@4NCAT0 znbefj>4BkCQD(F^bG@MFm-DW;W62#ibq;%fc(6lP;A$Lmo>AwQrLNPq#4aL6)K%v` z*Kz!VksE`>d(c5GO7Wg z1)e!kcx?Lm`4tde$Gwk)e&-wu)%p14@AnSs?6ug-(2_%lP zqBu`;Dw&dik#YoXL`kzgNz`i%q%hDRpWIRfatKsVX2xNQq^$Z=z z;oT_ed5t5BTn`#tvH5#7H*|kb$17c5cwrbu5C~6g>FN8U^kM(R7k0;8Zq|IP>(ZqE zhB@rb@^F-`K~jFUNBB_M7L0l^-HzS+B$|;iGe*MabBEln*~1$Ndo|Fhp}U3dMabCq z|H-Cc3DmKM4D)CbL-k4)lQ7;t==*} zP?|`CMTf_ns{$u=lw7*zojmykI4$il_EiU5gdeuaf>tKdexTw_JVDYaOUT86HaT`e z4|{SjCu$XjDsvmDkLhlFqimg(%H5(;28&~7UX-3k+oUvpVxb?BZjkHE))bC~{=No8 zMWDg5ZeFWT$f-U~vFCSsf|U1;&|CoYcspVR@U$Iw>P%A~7j$mXKHyciA(qj&2ZXEC zl;~pSjujy?D<(iqIeKUHHU*5%Og+?uSKF?|) z)`$waV`?hnpX5uac_vI+KE17&akBNVXk@)~I;a?m7+M82A=kHIaJiPzZn}J0RV(yd z2B?WR#io?@z#EyQ&H%cyV!LZHK_1$W8*{Jq&6w9Y(8bgZzGE^_v^EYcWeuDu5$#2Z zyY5oVbQ-(Lwh$05O)g%VQ)n}dI%EAB0lXX|;BZA!Il0E ztI(2U1Pzgo<$>1Ec!1s=?^`JF2D>~{D{*^Anh_@ZiSpBv>DM~b@^#<4Fe?Y_;^p2x zOD8yBJ1dr>f8HeA%2r%MhW~cx09tlqKBd= z+dVP1bFX#?%qJe&_WF$V!8-%(-YZM8%*EkYWP_TyoM2vR)pPa}RdU^);yAGq#P@7}kWcsAhL`%LcXh5~^oX1aIdbNg|fsV}> z0mP)s2apLI?})B5(iAu){}gFIqN_ls^+K6CrJXLum08l-G7RNSYpG++)i$a7hU4qeXg4iOurDbi{ON#cuhJ6K!|?I53Ou`$NVQu(J8oEqJD6 z``Ub@SCe*wn8kXE3exG%pV=GNWa&yN4b3~xDrG*^4&**QiPn*W$H{m9`Lv<%=|JZ0 z4c#Z_BLJM@(uCfCp=uo_Wy25m$KHP^v2GFL&ok>hUk1uFZ|B%#nKg2sb{T8{Bc}9z zx=hScG!!ZInJZdh_@zl#h&);Urn702WgI=%m~!r|)+koWMS z(#XC2xDUqJw;u-|LAC?qNhpR>8u$0H_KruZfvmG_WNn6V`63`E{!o9d+_}5Ak!}Pe zEIplKF!#FX(GvzZkKLfaHp0=$&H5Bwy(`1~?7~Yy)YCvxWeZP{obwOF3F)Y>fz~AH zOS%?_$r0WPkuA-atHN&rcQ~jjgu~ZK!1*`B#8y{u?7G z{ZC-lNt5F@jo`s^zSTYUrDuig8XmC~x<_}2z{Pp|w+`b|9x7I zeg4D8x&-pxteJ*m3v)RuS(4*lM&6lD*UiXjhI~ssKIPWBXlqK-Yt=zw8;L+{U&7+nzt5$IY>zGq8)_sqs8_kS@2=p%KHzkQ}|MRm&h=dipu-`KpB|11f!G zGU@rU1+M7|F(}?)0ytgJs&RY(QIHu|fVW5sGVMHt=D79o5eL2UN6$k6S_UJ~`)KBC zh$RxkTpbF)im~Ctky!LVm1gX@PMe4Y(AyM!Gtx_`J3jvAKD5(VKWNF}OeV1unu$q) z1pGHc{m&<0&bjR*wc$Vn(rATffhVZQqa7Vh{^yQ{JpB4~t;c1cyuT0d_4cT@J>tGA z{v|O~0ga=4V5M*s@1~&wq0Eji$|unsW=JoQ-c>*xJ8UordeSXIquIPhKui&vb)j`@ zHozXr&ua{X@ighc;S5WeutS9O3lpPBOx+*jtyV+$<@MahbIcm@opbp&Oycv{OJ6)OY-j&{`B}4@WJEgPgy5W#0 z7b<-o6w|gtK#9Q_2{f)0lo!ZaUNRdfECnU#WPp3+KA$~U?PY33FBJn&TL!PJ!`tu- zFyGT%BoU^(TbvF~>1UQz<+kr)r-Mpwn6~XYn0wN}PUEMg1@4mAQ*ge*TLkjVMOkwr z^$)pgT)V2U@a1b_L)^y?v<;nwm{Y{G;ts5_j?(*R^Mqy>rU&Tw!kx~E48wwasA-2- zS$g_ZnB@6)Qe&EOW50RllkTbA3PHCSm~pY94;O z?w*2@sFV5O!A4MPfJ{1r_OHCOdbeo+o+$_H&Tjyf)j6rUytj)O>&#-DBd%56qlfFC zV&vP^>nugKFn3T&^6oZQ9lL;}DnOGwoi$G*mZ_l6sPUL@_uGpv#>uI@6lo%wcnNmN zG{bd@NUuV3oE>R!1qE`T`t-QLZJ9&4z_i$KkAiSNmlpB!qqO_B5Tk0k7m@bXB4boXsM5{ z-ZpW3aXBx2#0 z?u!x3fc-^2v3?NO{Jyqqe+VqCK0+JC>|zueTn&)nx6#z~w#^pgLtrhSC8QftuR z!Bhw?;PE1L$8)Vyfc^>7l8S zJWr{9)U!3@X2M@~k~}6~BfM{W@$)Cirveo)(}BbkbZqT?(#e@;)Uyjn_l@nukBj#a z)VJN5Ig~HJ_+`(Ae(jI@sieZ7NKZh98nB=Zvk@mh#XO(k8XEnDWV(n2gYi{`gBX|; z#cii+VA;`x$wP;YTXcR+iiYSB9GA9agHvdda#(jj-@YvBjK-^9*_n7ZkZuyFYz`kI zE0g92lbf|&!e6n!q4t~h}dz*l4Q z<0^gogHm)KvJvgj#pR~CcmspAkX!j8xv^jKQ3q7DrvZt>)Fk-T^m zow%X;_;$a4K0Q+c#Ui+BHGbBtG!@j?1;l6&_jJ%tU;9xUgnX4gvxSMak@w-fR1sm# zpazYpBXITpFQvVPKv`(M+W#?j<$+MH-@lZ~mTpC%s9RE&6d}YYrLt$uHgq+VeH}4o zq=nl;g|TlXWSg;$bx?{L%am;}hGfe!Gxot)e$U+7e*6CFzZvz;`@YZfob#OLoX_WT zY>Oob!t9jUY*bW}fAI)`QDgAaFz$!fy{nmQRMgLBLG+(15GX zQ-sF5xv9U)(lia`#&@Ky?l?10BmeV6{0}pwKf@N;2NiaCaN@~ix1lCVtOoxP^5>dw zJjo9ieEB-CbwYr)_koIM9(x~BYT@8~V(I&rbs9%p%pwn83LrGJCvteHHRY3H1a2gy>+(iMcJ8i^HY}*NVsCGTrx>{m*$l5eQ2d+s z2zO8S+5Z~pf9?1*TdKIQwXnm38)LD&sYCs1PW<&dm+jfYb@BYdc-Z6{-+DKqF{}P3#Wh!*7kWtQnv=tfd2{0qrVdkl(PE=mER?tz2vQuRqapE+rN(bW~96P z@s8h_ln*SMyTr|ynk4UI>lI0@NUysleRwB^|937Xtk*1hdy1x7me>F?c)qZ*bXyvl zdK30r@0$PsO0JK+MJyk@po&Y2$dK>p-sJikbpCf!{%py~YJpxs<;2U3jB$dU>AY3W zi8ZbTKTje3$68PAQ3)#N%};-#R?gV~Hrr^O?1ufnGspmdW?}$^4Wz7Qw4CkQ_I&Q; ze!Jg!%{>qK!2l1!2Ew1umK0W49~GqRmi_!D-y7EtZ@h`M=)GLZMP6D3dN&@Lz?jMj zkrVzdH=DTi-^KbLBLMi7dlIju>$e%3`pC?xIKBA~uKQzc5M& z9O_^dXTkP!f-xocd+?VIc1<_68oq2V`N%y#^Bks7@JIQ;76^`sWrJGU55Hm>`BXZ71P_jMN9!IZ)WQm@^V4ZL~P<9`)|0Iwd*Uc!%FNm=Z{iSq7ilqo;D$|6MsEB1;ZF#pF+M=8rwnv@elp`6dPXqd)o95c$|O z`CLdTCvY^`9zK85w<^nmITze*{eFnbe){#GNuUg0gx;Y6m$h)6GcA!r!dVf){r!gz zeEr`h2LmIj;y`J&$_pe%;4m1>C8wy}1Hc1b`|^OgW1vN@O`u~}VFsvx-*arsM1#U- z3=@d4bpxF)38$20Rz9}fky2J<0Ye*$XUl>_8Uy49$tWfGcr>GfPrhOFA}B&JN3*cw z7oEwtMsxGoY>X{XA!HHtFs4|j3lMqB^4r(`zXsA{Q^W`_wtZZ5bJf{EpQ=5QO?gsQ zUnd7-LD(9JC#LTUDZ{h>;Z>~Nxa<99)%o}Frf#4Bs>DuV(dYnQaz7xJLAdlGGjG*t zfI5_&s{?W@@i%%oKd|!6RG>(Nu_$)I6c!$XGFZ$Kza>Csq5HATqEv^5~i1o^u-Wl&hLT3uQ0H)erGxaK7a{6Y04lqnH+vEo`D z0#G?903R5j9)UY@EIHDvXG{p%=y_esr|4JMeR zlF%)jUJylOHulG4!e4oOF@V6ow$As9tbX)k(45W2A*)LTfDlLQ28o`*9=|au7H7{) zaS;e%)Pcf$QLQ8^p%ZuPY8d6<1hu%g{ThU`S)u?s+?;JzP;%cN5-3{kFM#f4;YFPg09v8x>u-cp*#gH*DVZar^!xCGSi`-|ZdUKwQBh>v}ocJi);MkqsttBml6zk*7e!lD4NQ<-30fo0NEgJD*t+QgpmzbG$fB=ytA;avGl$Z_#p4CI8yN%-n`dVLE0O!mvvucNtr zf3gQvu*OQKA1iHHa%7Q_T9r|7fC?rY&N)6d31A@?uUC~Z%>XE_8`KUd7tZd6*%pH; zW)TfYX5lDTEEoG~f-TZk?Bb=dil4Or;+*Yl$)-N5ELzuhOYhUyR}%V>`6Yf4Etq!z zW2!PUzGKui<=ux5#@{9iIJfGXEvy1*P@GGaNsi@Md(JXGL5>UODApLKApNlfo%1w| zA(({+M$f+lNYw&;0f zKK$8=eCB?}RAM|cdE1vwD0ve*YH9MUL>tC~DqgSZIi#%sAHV1W3{r``6yRmX z+QIas(Ap@UBVUvJ*RQjRxUl?te|6@=XuzrEvk*hw8BPe`sDwhonpns%7r>wO2I*e! z1SRkM$@kJ_AOG|L9=r@B@BiQ1|M9m>H!_zJ)ZUr}@p;uFwL-HW_kL9>-1L>uHp1TH zKen6kd}c&7Zb?g__dyJ5E+!yz+VSwy7w=f*hjuF;E1?uC(ZRY9A^TyL5MT+|#;Tf# z8S=_TcbEVo`bvg|?(8YNYDlI4WZoEn1KXO>Z;!85+&e6BS+4#2#G2r3wL7ad0a=k! z6s0jQHz7Ek22{NCQjSaOvi-9*0@3G_wf~r%%Iiwes(-de?Fj$X%=c#LX4WGGYV6fdgFHDrJv#tU6^g#~?#1F$AX#e3RzEWwOK>f zpJbL)YfA^_S36}X3n~+EmLVVj`?wppyFOvxfj<;%s@ISx_WIe{&aJOoOw#K6%)eWQ zqC4I;wom(?$=6d|?$$vvcPn_d@1XV73HuZbH@G7RLlCK zi)RMcqbVx@q)|qKBBc7=2q^ul^CJEkew66axh}_Ypebyh9qy=MMq}Y|$Usqs!tyUH z$PsYM`d9gpnB})WWGD4~*cMgvtCUB5&MhC#=snctrCy98J^kba)Tr#5=vJ3bs_F;f z!NPROahHC$_!j!=+f17iUw4q?K6XIGx{MQ%8CV^Eu)j$0o^FCtDJYgVz>j*58Ywum z9`tFHBb5LFRsu^hiv|=D+<}(JBPmlGD6HSq%De5x<_r(&+1=g|>@xtZti=Fy=yIUc z#8)UK@q<5Vt-8)eSEiTT+*wc*=6D$ENkSAX)MQpsMMm2U#wRJMR9(@I8xk%BbO9Ft|BARJwq2s>;uo0bt^~ zQUGqlzPHUhh6F2`0^ndHGyuqFyx@{9u}}1^%^&6B>Du9c#N_n%N2X_ws^`cq{U#R9 za*U2!V$Rx(M)?uLl;zw%KF#)Nwn~=@*S#2*=Zvsx^5&`j?$N_vSqsT4>=9Qd@VqF; z1~_bQ>ls7E=OOZ4uS0}Yx>%Tdj3Ws{1KX{)2y*5T6M!kQ*H_d+1Ayw*KXhvI!YdbY zYT)A??|1}cUgq>^@r%2$uxX`0`U`8$w1Iu&uvzxD$oG~Wy=Y}v%Py{#Zvd7_y4;$MC@NMLJ6A9>ErA{IE9MO<}WmTC*@He5_`Ak=yl zL$Hew2Z!~Hhij~JR0?Fi8f4CTJK{<+j$Q_hGN+35)`|;Z&rT0DCe*^EpWe3dhmDE9 zPLz;^uJurH=<-^p=FGUzVq38L%k``PA~&DoR#p)|d~qy7?dqmy61E2m zkH@^xi(mw$ev_6ycB^^`fHG?eomKq zgCrtN)8EJ{(5K#0Z)2x=GXCYp4>ev1!Z8V74ai7*i_70e3tpdX=8GrwKC}fz;;urp z4_0=5;<>%r|9;hPte_(YRdJZCw_lc$qxE?H+U>vc`^>+d9DaDTZ@#{k0IcParPFjvM|{l9ys5i6Z2D=!Vk{?>Oq=(Hl~*Nm%IZmd2`v9=ZrD7UHYwD z=@Y=_-cT-l!HuGKRbyIpDsgIc-$!BJiQh_q8|)#tnHo_brzR*SucObqyg2qdFS_Ys z5JIL|Sf&e2!7NUR%ZW?@uW75?Px&9H2O3SZ6E>kb5((L%2p$2Fo|}FoGtB z4;pi^G!jAtWNS@Mj64mu1YvGw&);%Woc=&-h6Vt71t1tkl>uUrZ;!X>4EsW5@039H zxuhZ>*YlPoc~1j!z9B4fl<7?I-RbW$qIba#itd*JVknDU&Z^pAz)%LfUtLA@UFqcN zF#x0P130X|(Ms=G)RGdQm)gL#0~`%86SXP;XwH)QIeJqSx!B>s?#T-20b-?uPw9Q) znBd((B(LiX*mcL#v|~O1p3RUs&S?Zl+^ZJ@XnTo{eCPh`uxYtLx{@#iRRPXyq;3Gr z7zA9VF&3k&7{Cupfj-Aj_W>`UFP%PDFv|*s0R5_jwKcjpU09Qooh_MdGB^gHdvq32 zM8SW4LY&kH?ITEFy&y{yH-TWtEi0e7soEphJs*KgnPa*KkWOqrUyVFdLR-u?%~ia{ z(siBXcf*b<7gb`UOMCr$-z{*D_7_B+NGgk88M9s;1$yrv0UYU6HU99W&))KrWdqh2 z?7|U>ePPu6`!X5I^36Snbt9f%NMknaTlJ5q30wmr7w&+J_RMXlK2nMW3@JvjE1H_Y z(41q(jOrGKljtmTG%Iyse0%_ajh&u105(edZsg26zQ~q5fA$%H7W*8K2>=pTW#(cj z)nstc9R$KG7cZ5ww14L+tpRIH5faDJ(FxuSk4C!WD?q`YDBzSq-wZ}#{EYl6E5-sU zrmvBJ12gG?TxkXDCNUqtM=5713qm7R9^2saBY*&zi2^_)gK+q|!^Up!o7ZJ(0hHaA z0f5em%q=`hSo&I|NH(s^0Ry7yS-6*G1ik z>90eimb_&x`w8?b6u>E)%cJJx6)0Ze-f}_Jm*+~4T6+evh#^57`rfx$0B;u{A$QLQ z2hI#;YgPe*+Ji+*V}B2bowL_UHB0nBSTrQpT@9o_g{752gMtJXnNk)jtQW{xbB)$< zE#JC9APkw9lm$6n0a2Hf@Nu_fGAzo=RjJBR;j~h}{-R%)hR_@}ux9ub9jxbiEy}nf&Sr|NV5ui7i#>afE2T#7o+A*Yf?_ zT}Y0zA(t*DuJ&~3CQN^G*iC+-1{1(?UddcyY4zh@gth=d>XAp)z`xmS;d}_x(oPNo zzfbFW4`diB287t|7Ktk`ATGlKC9Q!YBmc%}ACe7P_h3|8;wtrg&dYCGf^a z#|Z?_EWLg$u?G~4l`_{caCUt}3s_}ZSD-vjbv`HTG0C74y1*gu^7B@fd|*blH;Au0 zwW1Ce<(1SE2>Clql_21F?*p=tWm33~7pw#@;ri-aaaB6%!4w%CzYUpjF(>QV?S?7evbd&{M((=((}lr5x_HGoBNpPUGpF%J1X_Z7oGx zybEq<;^~PchbXwuT zUF2hXAuoOs0|$wKT4biuqpT6-0YK03!TZh1*qV)P_&XDoR*22SDEqgw)b7c}F3%o5 zs_ZwKvmRCkx?q;+gAUWvDZz;!Z9ask_qEawn?BeRPoCS|<+*wa&hMVMW7 zqsdsoXnn5;YW{I1<+5ZFNKTM}%eMk_jf6^9W8_IlxuE#tOp`XBOdR$NBM#T{R)j^S z8CO2p%d?1T4LCbsDxy=PH4ZwCz|}(~XO?sB@}rBuH?m7wC+P1njAfe|eri>!S?7pgMqiRmM0<6A>PF`U&Ui#$ z>LHcU{DpBViAw|PHXIVE-aL3;28z6Xa2AvsIxpFShEI77tDf?RMo=XMOoSd z8Rk2By&DnXy5k$2I#T+cPy9YV+f)GR1j_qX<6~bUk#3k}}7+*{a z1)^IW9wHssTz^oPD1ZUIY|Gm))#`+1-(W{p#_GdW(1xl`_)rqn@{+Rm{Cg;2V@Cfh zG8vNCj#Hg3djVh32#(-&sT;c*H*gd=(J>c?zf|e?jK)mF_l`4;`yQ5=J=rp@ln{tjpq@mM3TKji2EgLQcGEZzI}0 zZrrGBxLEoT^lcz)jFxaR2`4s#{qXAWUX~hj*$dUSJb?BkE3xwOQjF*HYjFdJ9cf56 zQhS^XOtYc~Y)^5`!+WyB{TpXJuPC2`7<*GxZ4#$W65^S23VKhMLL_kw8*9SLi$=PZ zDwuH!28)|{NMTF93-=0liZp$ajA zIRdt6R#-B%6CYrZAWM0vEgox(Om81x>W~5emi}^ll!(dRM3Um-9?#p;Zy3vw#61gU z(+Xax14&+Og^MdwQB_9ABbEH^0vtfm>0a^q#1tG45Q`02ZagKh8q#t!tQWc1`EFRY zGgiUXrA`3oE9NT?x)L-O9{=eSgE^VB?Nf;AEu}9~< zHBk`^fERrdyqa({_N}X@Akj(+dfD?Ez(qDUTYudH9~Rs?LL%Cxx7srGnhv6SdS@}1LFfz+MEb?@E z&e0oaIgC>mE5d??lb`J>$rnLBtQD>6(Zv*itg3=b_wnI%`y(@wEYn6nuC)gV7s^~@xg^5;~SQpkCo^wrdNKmccSW+k=oGRWBz;EFoqzn7jXyU^qQqSkd~6U?&Aq$ zH7S#8zB;TNu$#L<2?w2*k>3S+l_@F{L7TD`$*TDeTm>fZQYZC0GxvG2q7Tg=^=Lz@ zfDJP|voc@(3^px+FiZLa7Mmr0L)$pWO5ph$I3PimgV?>M>~P6@HU!q2(iJ5Wg0{-a zhFjf>RlOP89{tflDRF4_I55c^J-yrh{2$OQ+hllS=qp2;viKibo<;9!y&Q)x?eStE8G2i8bgUo(xhKH$%o1wheU+36zxOp> zIb9^Yg>he1IrBy`5v?i>j*o_s2v|&$u~l&;@s!2_Ae_6P0c`TR%xh)%_{XJ?EuHDM z(?tuutKg(V9kzmq5bRLkHK?8|pvFRGWUcmJpH^@x0gSwo7kS=L^RzTNH!O0@p3uAW zeo5zmyJ#S%kV*4n`WDzjW}J4mQgCI#Ou+=GE}6G+!(1h{L9DaDu^_G2by2khNI6i; zEqjrwS6oS%m-vox-xS}r3-IFjDh;O+?DsA&Rvbh?lQAk z^>zOSP|5E$J?!f_-g9^H^|4JcO@yi0=h2)yMztI)4kSnyTU;`G?J<{HAR%dZRz_$A zlzuU}y06_xdFYdIln8-LZL3wife2|zBEE#+E25w0%nQ1s#gBoc30_HauK*C)=1 zb~~2?PE~q@R6GTo{dU>ws)Be@;GTMIzRWY^`vbNyc1@5~e{!+Rdv!zaz=wAZg^v%d zdyfmr_J0qtbqCINgo#o1gQW%Yw%hO0k|h+*red8-d9T<@GDN3ytlPNr&$s0`Lw>y1ojJ5Jcfr$^x9om8J)sV_WXYT|rRVdr8s{G;ZA z8pe67exMH7x$TXlg~+u=X#7zTRq5MyB@*N|UOk(n9RdC7wUx&VTZjfaGTq@DdD}RF z-e=h5VjVG+8LO}a^eQ+;zNDY6Olr%-kT+KLXWKw-ow@Bfp`aNZ*E2oN%>XWxC2!JX z1qmj6XmVMEwj4z_j=OgMy@MIHn$d$tkY$W6%A~hW_~tY*;(C+=_C+N`NtY(NpJ?0ZHI993wXQ;s}yxJ0Wbl zQ9Rh?0!0qn_BDW3Epnz`!z`4bX~LhIZ_{=Rmn(BY&aOZ^pA1;7Ia+3oKs*IH_d#vO9$<8eeujCor5!yq+m?0?=g?u}5tO1se`pKyiZhmS)cgn!P~l zB)%3hr=WXz{HU+XoU(pxGr|4^u8o43!t+-enUySd&#%E73%%~iSA}SG)@8n_$d9WT z-tUBE9J>6PDYW5~PpzXdDaXkj#-7#n>Wx@Zd|r;-G1Wx?57B>Yy}M7=nv&#WjGs{8 z>QF^@VzZ~l(>&khp%pQlTOJ?Urn)34Ap{|xMjsKcKh4WLUzr&jBvWdvc)AzT*|>2& zftO}cTq{p(6KCqSEy^vRTHpkJ5+hDTUadethtKGg9EUMqM%Iw)%Cm>NF^tz68>m}W zK_{qe!l~nl;?5s;(SZW{TfQoXiiHVeG&1!0W#C|ZfMWA~5K#$o_3-LlqUTjD?Dw*- zSlp|ZJ#|ynP;ed8wq!VS%cHzMWPeCo)*B~Fa%M{8LbnFD^Ia>32%Bwz_RTjy9!im9 zhu(kv&qe(DYs5^aJM-If0=UHWyWR#2nC^kXe2o7pQPP1m@_qaWd_|KG$M$MdMH}>D zqiL8bY0^lyDS|3>7#|)2t7RC3uCkl+I5k=gd@@mIF(oMftqw%|RYDessP^ht0o|3$OOk_KrukTT2%Xby?7&W> z{Sed1F?u}&Q^Aw)jyBp@-P}cnp7q1pwkDa#$D&4^E@sJ#ymoRP8RFfIpfpz*c5^ z>n1uIRO^luYF5(ycT03z{t+7I-_~ea*?gbkm3qm@-|kG&N*ua-N6Xi_`R5aln0{K} zT-X*pM%7Z9LH*J@WN=d2HwRcir~|{0xLfx*w@TQ_2ZSc|{^YDf$)J!?lDWpJ5-G7w zcD>OlH*r!~p9VWDO=3?A&e@{=1Gm82%9n6VWyS7J<*ckBlM6 zT03$lrTPz>gs+^!SOgjtJv`(p%YLG5Xjr8?{z=lTW$($piN5kbyYHv0xK$FyT>wn% zVvz~9Y^|D3e@U?I5c$$^l!vczZ$>^RWE+@=7WZSSrTI;0E0MYI6X5teYmYUJ^EH;@ z=(R>2WUD{O%WMhR1QJUzLSuCSq{f}4=2N@8)s0F{W*s=twF=6yJ4CY1+~dc<>LW`x zYl(cH{#M(!?Qes)Q`HoiPBak%B+&S!57YnphUpWiwBwxJUh(P+=c=uq>%ycQEg-sx z+nxpoGBg=3pi!6$U^`=TEAlt9JE$Al$XeH{y0UkNuN8#hOwvPkDK_7@z_}hIs$MTl z-j%qL``M2L2NV%)7IDy9b4E%rr|^jw{DHE{T!r41-80Y0P8Jtz@^(_!m|A z>+rF)M&#{QOM;HleYWC z4-l_-2lUk7(978$PrUFsGi5Itk3~g}W{xn=ntc?%fQ_@vY{^{IZL4%xT*h5~v(@MA z$&F^YR{d=f^(y5f2mWA>ADd=M?J|T+_c0}mF22hL;G#~&ad4P7X;tOJRwOXcSh zQz~WAN3lr@{;;JQ_i6m7vQP8V^K*$If^q~_$6Ob1to1agq~tDkev;0Q%s_f!x|BriNGN#%;=_ zu2iRk8bsGS&QdsRHaIy2bLKfz<DcDLq_LEy5tpCfY5{or)8#- zei3XDGLZa{OV|?Uey|X?vD#62Tcu=Bd~WJiM* zdyQTz2Lg_`8ArZxLa*%kIjLTs%19POYc2tTT=Z(RJI3DWEn5RR$p$c3a5M2uo3~V7Mfk~Mz6Z;Vr~v${>#|kPaaTX+vs%s4vv)U}uu0MG z)K63Dsn)wMq;NPki2wq74J`w_B6HKmE{aF<&!#FIrDjU9;s$^N^k*v z7jY*Ro=&ZZi{?b`KF67~j~(yfE5_KC20}N39;LJF)1Gqdr_?7bVrXypdu)-B$26|N z!%LgarVel1ylba4z}tm5KV$Rm28uTB0nu;e^9@-!c6FZ|YxPJUOq_O@GIqrN(0bt_ zEU4Tmm1@OBdR7u|nrxzjX1hnH%4J@^ z{Ns~*<8sWaK%ipM{>XwAXb@@f64r|aEtPpTCJ61wTuxiwf0J^lYEhRP%`rN~lv=#;<5O{Me>|Wxy^GnrYpLV;fbsolYN#(KZbYBd^Wk-SoNhqrF2jh3%2s3M3!af(KPBI{AYCFIVS&O=FYlzK$(f{I>H!8z<2N?cSK`Iyf2 z&yAc|AIMM}`O-!0Q*))c_=JxeCk8tf-7ChC^+GxrP>}=I_rY)EPyjXZ5*gwb|Ep>+b1tc14iMgy>RU(4=bPUTdF) z{peWA>{*@k`hJii6Pj9Iz@hD+HS5|W+NWLnXFzj!qR|d?-K}>Vx?9cDv}OhO3?E%< zzT0zUE$_AJh;M_fuwmIHJ=nFf zaR?~%ZYV!@WdZa=uDCB7e|NHKA+Ab5vYuzl$YE8f0~xiL=SHVKGVpZd&0H#famlOq z-Z9TeW;kn!!g7iG<@^r38%hsAr(;Rn+0$LOsCl|;BFByeIu(m@>zOu<(3Bwkf;d=$ z(f;(&V^VDk0<>h@lF;ZOJzq`VSWFVD5nk&V(PZM{h@diwxKCu^_4E89?U$k_Eqlax zkssBgx?3Hvski*){Iz-iD4K#mYCsdB$Cg~)+RQ~ys_?mGfddf@M1)G0W|OkQsa7F3 z4_PgRY+c4`DZd6gO|hx1vRBs52fr`?bk(jxr^2Enk5kvb@vVBQLUVuMSVlr(G8QXY zO$)5c{=i~N`KNm_$Q@J;H=uHS*YN<*^GJeTWOTtNN{SKFMHzHZ?e*|H zNyQJ`20fyZpFom)R+osY-TQNOQv$|L9Z@G%rMLcU##>v4vG3V3RCk5KR81H3}z;4I#@XQr%2sArxRPZF7q?O7+X zh`@`f%<}r_p%(keb#=Ja9@3{CW>^21r`-`_Or<(o1n#Jq;lumq*gMwrb!&Ax-b{KF zx7<;uY7%LmARld>tD9IHc-pKIBqe9Na+66L?sK(YE$`i3QJJ%Iya+vbbI`fX-mE?7 z0UlRBnx7*CUL6m=-#vAL*+8m?&gDOM96c>m%L^rM(D#O8sRrc@uH{ce8nU)It5F

LD|&1cLkhZ$hd1=G*F6$*jw0Z3_ZFV# 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" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@prisma/client": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.9.0.tgz", + "integrity": "sha512-Gg7j1hwy3SgF1KHrh0PZsYvAaykeR0PaxusnLXydehS96voYCGt1U5zVR31NIouYc63hWzidcrir1a7AIyCsNQ==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.9.0.tgz", + "integrity": "sha512-Wcfk8/lN3WRJd5w4jmNQkUwhUw0eksaU/+BlAJwPQKW10k0h0LC9PD/6TQFmqKVbHQL0vG2z266r0S1MPzzhbA==", + "license": "Apache-2.0", + "dependencies": { + "jiti": "2.4.2" + } + }, + "node_modules/@prisma/debug": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.9.0.tgz", + "integrity": "sha512-bFeur/qi/Q+Mqk4JdQ3R38upSYPebv5aOyD1RKywVD+rAMLtRkmTFn28ZuTtVOnZHEdtxnNOCH+bPIeSGz1+Fg==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.9.0.tgz", + "integrity": "sha512-im0X0bwDLA0244CDf8fuvnLuCQcBBdAGgr+ByvGfQY9wWl6EA+kRGwVk8ZIpG65rnlOwtaWIr/ZcEU5pNVvq9g==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.9.0", + "@prisma/engines-version": "6.9.0-10.81e4af48011447c3cc503a190e86995b66d2a28e", + "@prisma/fetch-engine": "6.9.0", + "@prisma/get-platform": "6.9.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "6.9.0-10.81e4af48011447c3cc503a190e86995b66d2a28e", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.9.0-10.81e4af48011447c3cc503a190e86995b66d2a28e.tgz", + "integrity": "sha512-Qp9gMoBHgqhKlrvumZWujmuD7q4DV/gooEyPCLtbkc13EZdSz2RsGUJ5mHb3RJgAbk+dm6XenqG7obJEhXcJ6Q==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.9.0.tgz", + "integrity": "sha512-PMKhJdl4fOdeE3J3NkcWZ+tf3W6rx3ht/rLU8w4SXFRcLhd5+3VcqY4Kslpdm8osca4ej3gTfB3+cSk5pGxgFg==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.9.0", + "@prisma/engines-version": "6.9.0-10.81e4af48011447c3cc503a190e86995b66d2a28e", + "@prisma/get-platform": "6.9.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.9.0.tgz", + "integrity": "sha512-/B4n+5V1LI/1JQcHp+sUpyRT1bBgZVPHbsC4lt4/19Xp4jvNIVcq5KYNtQDk5e/ukTSjo9PZVAxxy9ieFtlpTQ==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.9.0" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.6.tgz", + "integrity": "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "license": "MIT" + }, + "node_modules/@types/is-email": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/is-email/-/is-email-1.0.0.tgz", + "integrity": "sha512-b/76ooKpYY/b+oPrOuc/pmM5eag+ZlzctPsKcRCIKs+TFzh0FL58OeXtSPkbXt3uKNK84YCKHmjnoREtwve5Kg==", + "license": "MIT" + }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "license": "MIT" + }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/multer": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.13.tgz", + "integrity": "sha512-bhhdtPw7JqCiEfC9Jimx5LqX9BDIPJEh2q/fQ4bqbBPtyEZYr3cvF22NwG0DmPZNYA0CAf2CnqDB4KIGGpJcaw==", + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/node": { + "version": "24.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.1.tgz", + "integrity": "sha512-MX4Zioh39chHlDJbKmEgydJDS3tspMP/lnQC67G3SWsTnb9NeYVWOjkxpOSy4oMfPs4StcWHwBrvUb4ybfnuaw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-xevGOReSYGM7g/kUBZzPqCrR/KYAo+F0yiPc85WFTJa0MSLtyFTVTU6cJu/aV4mid7IffDIWqo69THF2o4JiEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/strip-json-comments": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", + "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "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/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "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/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "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": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "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.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.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/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "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/dynamic-dedupe": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", + "integrity": "sha512-ssuANeD+z97meYOqd50e04Ze5qp4bPqo8cCkI4TRjZkzAUgIDTrXV1R8QCdINpiI+hw14+rYazvTRdQrz0/rFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "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": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data/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/form-data/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/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": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "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/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-email": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-email/-/is-email-1.0.2.tgz", + "integrity": "sha512-UojUgD2EhDTBQ2SGKwrK9edce5phRzgLsP+V5+Uu2Swi+uvjVXgH3zduM3HhT9iaC/9Kq19/TYUbP0jPoi6ioA==", + "license": "SEE LICENSE IN LICENSE" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "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": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/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/morgan/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/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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/multer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.1.tgz", + "integrity": "sha512-Ug8bXeTIUlxurg8xLTEskKShvcKDZALo1THEX5E41pYCD2sCVub5/kIRIGqWNoqV6szyLyQKV6mD4QUrWE5GCQ==", + "license": "MIT", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.6.0", + "concat-stream": "^2.0.0", + "mkdirp": "^0.5.6", + "object-assign": "^4.1.1", + "type-is": "^1.6.18", + "xtend": "^4.0.2" + }, + "engines": { + "node": ">= 10.16.0" + } + }, + "node_modules/multer/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/multer/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/multer/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/multer/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/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "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-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prettier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", + "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prisma": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.9.0.tgz", + "integrity": "sha512-resJAwMyZREC/I40LF6FZ6rZTnlrlrYrb63oW37Gq+U+9xHwbyMSPJjKtM7VZf3gTO86t/Oyz+YeSXr3CmAY1Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.9.0", + "@prisma/engines": "6.9.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "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/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "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": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "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/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "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.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "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/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "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/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superstruct": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", + "integrity": "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.24.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.24.1.tgz", + "integrity": "sha512-ITeWc7CCAfK53u8jnV39UNqStQZjSt+bVYtJHsOEL3vVj/WV9/8HmsF8Ej4oD8r+Xk1HpWyeW/t59r1QNeAcUQ==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.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/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node-dev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-2.0.0.tgz", + "integrity": "sha512-ywMrhCfH6M75yftYvrvNarLEY+SUXtUvU8/0Z6llrHQVBx12GiFk5sStF8UdfE/yfzk9IAq7O5EEbTQsxlBI8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.1", + "dynamic-dedupe": "^0.3.0", + "minimist": "^1.2.6", + "mkdirp": "^1.0.4", + "resolve": "^1.0.0", + "rimraf": "^2.6.1", + "source-map-support": "^0.5.12", + "tree-kill": "^1.2.2", + "ts-node": "^10.4.0", + "tsconfig": "^7.0.0" + }, + "bin": { + "ts-node-dev": "lib/bin.js", + "tsnd": "lib/bin.js" + }, + "engines": { + "node": ">=0.8.0" + }, + "peerDependencies": { + "node-notifier": "*", + "typescript": "*" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/ts-node-dev/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tsconfig": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", + "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/strip-bom": "^3.0.0", + "@types/strip-json-comments": "0.0.30", + "strip-bom": "^3.0.0", + "strip-json-comments": "^2.0.0" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "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/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "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/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "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/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "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" + } + }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..96ad3be8 --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "ts-sprint11-be", + "version": "1.0.0", + "main": "app.js", + "scripts": { + "dev": "ts-node-dev --respawn src/main.ts", + "build": "tsc", + "start": "node --enable-source-maps dist/main.js", + "migrate": "prisma migrate dev", + "studio": "prisma studio", + "postinstall": "prisma generate" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "@prisma/client": "^6.9.0", + "@types/cors": "^2.8.19", + "@types/is-email": "^1.0.0", + "@types/jsonwebtoken": "^9.0.10", + "@types/morgan": "^1.9.10", + "@types/multer": "^1.4.13", + "@types/swagger-ui-express": "^4.1.8", + "axios": "^1.9.0", + "cors": "^2.8.5", + "dotenv": "^16.5.0", + "express": "^5.1.0", + "is-email": "^1.0.2", + "jsonwebtoken": "^9.0.2", + "morgan": "^1.10.0", + "multer": "^2.0.1", + "prettier": "^3.5.3", + "prisma": "^6.9.0", + "superstruct": "^2.0.2", + "swagger-ui-express": "^5.0.1", + "yaml": "^2.8.0" + }, + "devDependencies": { + "@types/express": "^5.0.3", + "nodemon": "^3.1.10", + "ts-node": "^10.9.2", + "ts-node-dev": "^2.0.0", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.8.3" + }, + "_moduleAliases": { + "@": "dist" + } +} diff --git a/prisma/migrations/20250613074707_new_table/migration.sql b/prisma/migrations/20250613074707_new_table/migration.sql new file mode 100644 index 00000000..ed8395ae --- /dev/null +++ b/prisma/migrations/20250613074707_new_table/migration.sql @@ -0,0 +1,107 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "password" TEXT NOT NULL, + "nickname" TEXT NOT NULL, + "image" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Article" ( + "id" SERIAL NOT NULL, + "writerId" INTEGER NOT NULL, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "image" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Article_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Product" ( + "id" SERIAL NOT NULL, + "ownerId" INTEGER NOT NULL, + "name" TEXT NOT NULL, + "description" TEXT NOT NULL, + "price" INTEGER NOT NULL, + "tags" TEXT[], + "images" TEXT[], + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Product_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Comment" ( + "id" SERIAL NOT NULL, + "writerId" INTEGER NOT NULL, + "content" TEXT NOT NULL, + "productId" INTEGER, + "articleId" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Comment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Like" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "productId" INTEGER, + "articleId" INTEGER, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Like_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RefreshToken" ( + "id" SERIAL NOT NULL, + "userId" INTEGER NOT NULL, + "token" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "RefreshToken_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Like_userId_productId_key" ON "Like"("userId", "productId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Like_userId_articleId_key" ON "Like"("userId", "articleId"); + +-- AddForeignKey +ALTER TABLE "Article" ADD CONSTRAINT "Article_writerId_fkey" FOREIGN KEY ("writerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Product" ADD CONSTRAINT "Product_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_writerId_fkey" FOREIGN KEY ("writerId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Comment" ADD CONSTRAINT "Comment_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Like" ADD CONSTRAINT "Like_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Like" ADD CONSTRAINT "Like_productId_fkey" FOREIGN KEY ("productId") REFERENCES "Product"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Like" ADD CONSTRAINT "Like_articleId_fkey" FOREIGN KEY ("articleId") REFERENCES "Article"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..044d57cd --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/prisma/mocks/articleMocks.js b/prisma/mocks/articleMocks.js new file mode 100644 index 00000000..9c8ec09f --- /dev/null +++ b/prisma/mocks/articleMocks.js @@ -0,0 +1,32 @@ +export const ArticleMocks = [ + { + writerId: 1, + title: 'Article 1', + content: 'Article 1 content', + image: null, + }, + { + writerId: 1, + title: 'Article 2', + content: 'Article 2 content', + image: null, + }, + { + writerId: 1, + title: 'Article 3', + content: 'Article 3 content', + image: null, + }, + { + writerId: 1, + title: 'Article 4', + content: 'Article 4 content', + image: null, + }, + { + writerId: 1, + title: 'Article 5', + content: 'Article 5 content', + image: null, + }, +]; diff --git a/prisma/mocks/comments.js b/prisma/mocks/comments.js new file mode 100644 index 00000000..600d6f68 --- /dev/null +++ b/prisma/mocks/comments.js @@ -0,0 +1,62 @@ +export const CommentMocks = [ + { + writerId: 1, + content: 'Article 1 Comment 1', + articleId: 1, + }, + { + writerId: 1, + content: 'Article 1 Comment 2', + articleId: 1, + }, + { + writerId: 1, + content: 'Article 2 Comment 1', + articleId: 2, + }, + { + writerId: 1, + content: 'Article 3 Comment 1', + articleId: 3, + }, + { + writerId: 1, + content: 'Article 4 Comment 1', + articleId: 4, + }, + { + writerId: 1, + content: 'Article 5 Comment 1', + articleId: 5, + }, + { + writerId: 1, + content: 'Product 1 Comment 1', + productId: 1, + }, + { + writerId: 1, + content: 'Product 1 Comment 2', + productId: 1, + }, + { + writerId: 1, + content: 'Product 2 Comment 1', + productId: 2, + }, + { + writerId: 1, + content: 'Product 3 Comment 1', + productId: 3, + }, + { + writerId: 1, + content: 'Product 4 Comment 1', + productId: 4, + }, + { + writerId: 1, + content: 'Product 5 Comment 1', + productId: 5, + }, +]; diff --git a/prisma/mocks/likeMocks.js b/prisma/mocks/likeMocks.js new file mode 100644 index 00000000..6685ac7c --- /dev/null +++ b/prisma/mocks/likeMocks.js @@ -0,0 +1,14 @@ +export const LikeMocks = [ + { + userId: 1, + productId: 1, + }, + { + userId: 1, + productId: 2, + }, + { + userId: 1, + articleId: 1, + }, +]; diff --git a/prisma/mocks/productMocks.js b/prisma/mocks/productMocks.js new file mode 100644 index 00000000..4124ede5 --- /dev/null +++ b/prisma/mocks/productMocks.js @@ -0,0 +1,42 @@ +export const ProductMocks = [ + { + ownerId: 1, + name: 'Product 1', + description: 'Product 1 description', + price: 10, + tags: ['tag1'], + images: [], + }, + { + ownerId: 1, + name: 'Product 2', + description: 'Product 2 description', + price: 20, + tags: ['tag1', 'tag2'], + images: [], + }, + { + ownerId: 1, + name: 'Product 3', + description: 'Product 3 description', + price: 30, + tags: ['tag3'], + images: [], + }, + { + ownerId: 1, + name: 'Product 4', + description: 'Product 4 description', + price: 40, + tags: [], + images: [], + }, + { + ownerId: 1, + name: 'Product 5', + description: 'Product 5 description', + price: 50, + tags: [], + images: [], + }, +]; diff --git a/prisma/mocks/userMocks.js b/prisma/mocks/userMocks.js new file mode 100644 index 00000000..e249685c --- /dev/null +++ b/prisma/mocks/userMocks.js @@ -0,0 +1,10 @@ +import { UserPasswordBuilder } from '../../src/infra/UserPasswordBuilder.js'; + +export const UserMocks = [ + { + email: 'firstUser@pandamarket.com', + password: UserPasswordBuilder.hashPassword('password'), + nickname: 'firstUser', + image: null, + }, +]; diff --git a/prisma/prismaClient.ts b/prisma/prismaClient.ts new file mode 100644 index 00000000..0a7fbaa8 --- /dev/null +++ b/prisma/prismaClient.ts @@ -0,0 +1,2 @@ +import { PrismaClient } from '@prisma/client'; +export const prisma = new PrismaClient(); diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 00000000..098268e3 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,90 @@ +// This is your Prisma schema file, +// learn more about it in the docs: https://pris.ly/d/prisma-schema + +// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? +// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init + +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + password String + nickname String + image String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + Article Article[] + Product Product[] + Comment Comment[] + Like Like[] +} + +model Article { + id Int @id @default(autoincrement()) + writer User @relation(fields: [writerId], references: [id], onDelete: Cascade) + writerId Int + title String + content String + image String? + ArticleComment Comment[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + likes Like[] +} + +model Product { + id Int @id @default(autoincrement()) + owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade) + ownerId Int + name String + description String + price Int + tags String[] + images String[] + ProductComment Comment[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + likes Like[] +} + +model Comment { + id Int @id @default(autoincrement()) + writer User @relation(fields: [writerId], references: [id], onDelete: Cascade) + writerId Int + content String + product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) + productId Int? + article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade) + articleId Int? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Like { + id Int @id @default(autoincrement()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + userId Int + product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) + productId Int? + article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade) + articleId Int? + createdAt DateTime @default(now()) + + @@unique([userId, productId]) + @@unique([userId, articleId]) +} + +model RefreshToken { + id Int @id @default(autoincrement()) + userId Int + token String + createdAt DateTime @default(now()) +} diff --git a/prisma/seed.js b/prisma/seed.js new file mode 100644 index 00000000..fad204f5 --- /dev/null +++ b/prisma/seed.js @@ -0,0 +1,37 @@ +import { PrismaClient } from '@prisma/client'; +import { ArticleMocks } from './mocks/articleMocks.js'; +import { ProductMocks } from './mocks/productMocks.js'; +import { CommentMocks } from './mocks/comments.js'; + +const prisma = new PrismaClient(); + +async function main() { + // 기존 데이터 삭제 + await prisma.article.deleteMany(); + await prisma.product.deleteMany(); + await prisma.comment.deleteMany(); + + // 목 데이터 삽입 + await prisma.article.createMany({ + data: ArticleMocks, + skipDuplicates: true, + }); + await prisma.product.createMany({ + data: ProductMocks, + skipDuplicates: true, + }); + await prisma.comment.createMany({ + data: CommentMocks, + skipDuplicates: true, + }); +} + +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/src/application/article/CreateArticleCommentHandler.ts b/src/application/article/CreateArticleCommentHandler.ts new file mode 100644 index 00000000..a3c15b39 --- /dev/null +++ b/src/application/article/CreateArticleCommentHandler.ts @@ -0,0 +1,64 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { Comment } from '../../domain/Comment'; +import { User } from '../../domain/User'; +import { TArticle, TArticleUser } from '@/types/article'; + +type TCreateArticleComment = { + articleId : number; + content: string; +} + +export class CreateArticleCommentHandler { + static async handle(requester: TArticleUser, { articleId, content }: TCreateArticleComment) { + /** + * [게시글 댓글 등록 트랜잭션] + * + * 1. 게시글이 존재하는지 확인합니다. + * 2. 게시글이 존재한다면, 댓글을 등록합니다. + */ + const commentEntity = await prismaClient.$transaction(async (tx) => { + const targetArticleEntity = await tx.article.findUnique({ + where: { + id: articleId, + }, + }); + + if (!targetArticleEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.ARTICLE_NOT_FOUND); + } + + return await tx.comment.create({ + data: { + articleId, + writerId: requester.userId, + content, + }, + }); + }); + + const comment = new Comment(commentEntity); + + const writerEntity = await prismaClient.user.findUnique({ + where: { + id: comment.getWriterId(), + }, + }); + + const writer = new User(writerEntity); + + return { + id: comment.getId(), + writer: { + id: writer.getId(), + nickname: writer.getNickname(), + image: writer.getImage(), + }, + content: comment.getContent(), + createdAt: comment.getCreatedAt(), + }; + } +} diff --git a/src/application/article/CreateArticleHandler.ts b/src/application/article/CreateArticleHandler.ts new file mode 100644 index 00000000..b481343a --- /dev/null +++ b/src/application/article/CreateArticleHandler.ts @@ -0,0 +1,54 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { Article } from '../../domain/Article'; +import { User } from '../../domain/User'; +import { TArticle, TArticleUser } from '@/types/article'; + +type TCreateArticle = { + title: string; + content: string; + image: string | null; +} + +export class CreateArticleHandler { + static async handle(requester: TArticleUser, { title, content, image } : TCreateArticle) { + const articleEntity = await prismaClient.article.create({ + data: { + writerId: requester.userId, + title, + content, + image, + }, + }); + + /** + * [클래스 객체로 변환] + * + * articleEntity 는 Article 클래스의 인스턴스가 아니므로, + * Article 클래스에 정의된 메서드를 사용할 수 없습니다. + */ + const article = new Article(articleEntity); + + const writerEntity = await prismaClient.user.findUnique({ + where: { + id: article.getWriterId(), + }, + }); + + const writer = new User(writerEntity); + + return { + id: article.getId(), + writer: { + id: writer.getId(), + nickname: writer.getNickname(), + image: writer.getImage(), + }, + title: article.getTitle(), + content: article.getContent(), + image: article.getImage(), + createdAt: article.getCreatedAt(), + updatedAt: article.getUpdatedAt(), + }; + } +} diff --git a/src/application/article/CreateArticleLikeHandler.ts b/src/application/article/CreateArticleLikeHandler.ts new file mode 100644 index 00000000..20121543 --- /dev/null +++ b/src/application/article/CreateArticleLikeHandler.ts @@ -0,0 +1,70 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { Article } from '../../domain/Article'; +import { User } from '../../domain/User'; +import { TArticle, TArticleUser } from '@/types/article'; + +type TCreateArticleLike = { + articleId: number; +} + + +export class CreateArticleLikeHandler { + static async handle(requester: TArticleUser, { articleId } : TCreateArticleLike) { + const articleEntity = await prismaClient.$transaction(async (tx) => { + const targetArticleEntity = await tx.article.findUnique({ + where: { + id: articleId, + }, + }); + + if (!targetArticleEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.ARTICLE_NOT_FOUND); + } + + const likeEntity = await tx.like.findUnique({ + where: { + userId_articleId: { + userId: requester.userId, + articleId, + }, + }, + }); + + if (!likeEntity) { + await tx.like.create({ + data: { + userId: requester.userId, + articleId, + }, + }); + } + + return targetArticleEntity; + }); + const article = new Article(articleEntity); + + const writerEntity = await prismaClient.user.findUnique({ + where: { + id: articleEntity.writerId, + }, + }); + const writer = new User(writerEntity); + + return { + id: article.getId(), + writer: { + id: writer.getId(), + nickname: writer.getNickname(), + }, + title: article.getTitle(), + content: article.getContent(), + image: article.getImage(), + createdAt: article.getCreatedAt(), + isFavorite: true, + }; + } +} diff --git a/src/application/article/DeleteArticleHandler.ts b/src/application/article/DeleteArticleHandler.ts new file mode 100644 index 00000000..237829bb --- /dev/null +++ b/src/application/article/DeleteArticleHandler.ts @@ -0,0 +1,36 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ForbiddenException } from '../../exceptions/ForbiddenException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; +import { TArticle, TArticleUser } from '@/types/article'; + +type TDeleteArticle = { + articleId: number; +} + +export class DeleteArticleHandler { + static async handle(requester: TArticleUser, { articleId } : TDeleteArticle) { + await prismaClient.$transaction(async (tx) => { + const targetArticleEntity = await tx.article.findUnique({ + where: { + id: articleId, + }, + }); + + if (!targetArticleEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.ARTICLE_NOT_FOUND); + } + + if (targetArticleEntity.writerId !== requester.userId) { + throw new ForbiddenException('Forbidden', ExceptionMessage.FORBIDDEN); + } + + return await tx.article.delete({ + where: { + id: articleId, + }, + }); + }); + } +} diff --git a/src/application/article/DeleteArticleLikeHandler.ts b/src/application/article/DeleteArticleLikeHandler.ts new file mode 100644 index 00000000..49d6dc78 --- /dev/null +++ b/src/application/article/DeleteArticleLikeHandler.ts @@ -0,0 +1,72 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { Article } from '../../domain/Article'; +import { User } from '../../domain/User'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; +import { TArticle, TArticleUser } from '@/types/article'; + +type DeleteArticleLike = { + articleId: number; +} + +export class DeleteArticleLikeHandler { + static async handle(requester: TArticleUser, { articleId } : DeleteArticleLike) { + const articleEntity = await prismaClient.$transaction(async (tx) => { + const targetArticleEntity = await tx.article.findUnique({ + where: { + id: articleId, + }, + }); + + if (!targetArticleEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.ARTICLE_NOT_FOUND); + } + + const likeEntity = await tx.like.findUnique({ + where: { + userId_articleId: { + userId: requester.userId, + articleId, + }, + }, + }); + + if (likeEntity) { + await tx.like.delete({ + where: { + userId_articleId: { + userId: requester.userId, + articleId, + }, + }, + }); + } + + return targetArticleEntity; + }); + + const article = new Article(articleEntity); + + const writerEntity = await prismaClient.user.findUnique({ + where: { + id: articleEntity.writerId, + }, + }); + const writer = new User(writerEntity); + + return { + id: article.getId(), + writer: { + id: writer.getId(), + nickname: writer.getNickname(), + }, + title: article.getTitle(), + content: article.getContent(), + image: article.getImage(), + createdAt: article.getCreatedAt(), + isFavorite: false, + }; + } +} diff --git a/src/application/article/GetArticleCommentListHandler.ts b/src/application/article/GetArticleCommentListHandler.ts new file mode 100644 index 00000000..6cc8e257 --- /dev/null +++ b/src/application/article/GetArticleCommentListHandler.ts @@ -0,0 +1,78 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { Comment } from '../../domain/Comment'; +import { User } from '../../domain/User'; + +type TGetArticleCommentList = { + articleId: number; + cursor?: number | undefined; + take?: number | undefined; +} + +export class GetArticleCommentListHandler { + static async handle({ articleId, cursor, take } : TGetArticleCommentList) { + const commentEntities = await prismaClient.$transaction(async (tx) => { + const targetArticleEntity = await tx.article.findUnique({ + where: { + id: articleId, + }, + }); + + if (!targetArticleEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.ARTICLE_NOT_FOUND); + } + + return await tx.comment.findMany({ + cursor: cursor + ? { + id: cursor, + } + : undefined, + take: Number(take) + 1, + where: { + articleId: articleId, + }, + }); + }); + + const comments = commentEntities.map((commentEntity) => new Comment(commentEntity)); + + const writerEntities = await prismaClient.user.findMany({ + where: { + id: { + in: Array.from(new Set(comments.map((comment) => comment.getWriterId()))), + }, + }, + }); + + const writers = writerEntities.map((writerEntity) => new User(writerEntity)); + + const hasNext = comments.length === Number(take) + 1; + + return { + data: comments.slice(0, take).map((comment) => { + const writer = writers.find((writer) => writer.getId() === comment.getWriterId()); + + if (!writer) { + throw new Error('Writer not found'); + } + return { + id: comment.getId(), + writer: { + id: writer.getId(), + nickname: writer.getNickname(), + image: writer.getImage(), + }, + articleId: comment.getArticleId(), + content: comment.getContent(), + createdAt: comment.getCreatedAt(), + }; + }), + hasNext, + nextCursor: hasNext ? comments[comments.length - 1].getId() : null, + }; + } +} diff --git a/src/application/article/GetArticleHandler.ts b/src/application/article/GetArticleHandler.ts new file mode 100644 index 00000000..73d0e69a --- /dev/null +++ b/src/application/article/GetArticleHandler.ts @@ -0,0 +1,59 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { Article } from '../../domain/Article'; +import { User } from '../../domain/User'; +import { TArticle, TArticleUser } from '@/types/article'; + +type TGetArticle = { + articleId: number; +} + +export class GetArticleHandler { + static async handle(requester: TArticleUser, { articleId } : TGetArticle) { + const articleEntity = await prismaClient.article.findUnique({ + where: { + id: Number(articleId), // params 에서 가져온 값은 문자열이므로, 여기서는 숫자로 변환하여 사용해야 합니다. + }, + include: { + likes: { + select: { + // 좋아요의 id, userId만 필요함 + id: true, + userId: true, + }, + }, + }, + }); + + if (!articleEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.ARTICLE_NOT_FOUND); + } + + const article = new Article(articleEntity); + + const writerEntity = await prismaClient.user.findUnique({ + where: { + id: article.getWriterId(), + }, + }); + + const writer = new User(writerEntity); + + return { + id: article.getId(), + writer: { + id: writer.getId(), + nickname: writer.getNickname(), + }, + title: article.getTitle(), + content: article.getContent(), + image: article.getImage(), + createdAt: article.getCreatedAt(), + favoriteCount: article.getFavoriteCount(), + isFavorite: article.getIsFavorite(requester.userId), + }; + } +} diff --git a/src/application/article/GetArticleListHandler.ts b/src/application/article/GetArticleListHandler.ts new file mode 100644 index 00000000..08461bc5 --- /dev/null +++ b/src/application/article/GetArticleListHandler.ts @@ -0,0 +1,94 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { Article } from '../../domain/Article'; +import { User } from '../../domain/User'; +import { Like } from '../../domain/Like'; +import { TArticle, TArticleUser } from '@/types/article'; +import { Prisma } from '@prisma/client'; + +type TetArticleList = { + cursor?: number | undefined; + limit?: number | undefined; + orderBy? : 'favorite' | 'recent'; + keyword? : string | undefined; +} + +export class GetArticleListHandler { + static async handle(requester: TArticleUser, { cursor, limit, orderBy, keyword } : TetArticleList) { + const orderByOption = + orderBy === 'favorite' + ? { _count: { likes: 'desc' as Prisma.SortOrder } } + : { createdAt: 'desc' as Prisma.SortOrder }; + + const articleEntities = await prismaClient.article.findMany({ + cursor: cursor + ? { + id: cursor, + } + : undefined, + take: Number(limit) + 1, + orderBy: orderByOption, + where: { + title: keyword ? { contains: keyword } : undefined, + }, + }); + + const articles = articleEntities.map( + (articleEntity) => new Article(articleEntity) + ); + + const writerEntities = await prismaClient.user.findMany({ + where: { + id: { + in: Array.from( + new Set(articles.map((article) => article.getWriterId())) + ), + }, + }, + }); + + const writers = writerEntities.map( + (writerEntity) => new User(writerEntity) + ); + + const likeEntities = await prismaClient.like.findMany({ + where: { + userId: requester.userId, + articleId: { + in: Array.from(new Set(articles.map((article) => article.getId()))), + }, + }, + }); + + const likes = likeEntities.map((likeEntity) => new Like(likeEntity)); + + const hasNext = articles.length === Number(limit) + 1; + + return { + list: articles.slice(0, limit).map((article) => { + const writer = writers.find( + (writer) => writer.getId() === article.getWriterId() + ); + const like = likes.find( + (like) => like.getArticleId() === article.getId() + ); + if(!writer) { + throw new Error('작성자를 찾을 수 없습니다.') + } + return { + id: article.getId(), + writer: { + id: writer.getId(), + nickname: writer.getNickname(), + }, + title: article.getTitle(), + content: article.getContent(), + image: article.getImage(), + createdAt: article.getCreatedAt(), + isFavorite: !!like, + }; + }), + nextCursor: hasNext ? articles[articles.length - 1].getId() : null, + }; + } +} diff --git a/src/application/article/UpdateArticleHandler.ts b/src/application/article/UpdateArticleHandler.ts new file mode 100644 index 00000000..512c37bf --- /dev/null +++ b/src/application/article/UpdateArticleHandler.ts @@ -0,0 +1,77 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ForbiddenException } from '../../exceptions/ForbiddenException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { Article } from '../../domain/Article'; +import { User } from '../../domain/User'; +import { TArticle, TArticleUser } from '@/types/article'; + +type TUpdateArticle = { + articleId: number; + title: string | undefined; + content: string | undefined; + image: string | undefined | null; +} + +export class UpdateArticleHandler { + static async handle(requester: TArticleUser, { articleId, title, content, image } : TUpdateArticle) { + /** + * [게시글 수정 트랜잭션] + * + * 1. 게시글을 수정하기 전에 해당 게시글이 존재하는지 확인합니다. + * 2. 게시글이 존재한다면, 게시글을 수정합니다. + * + * update() 하나만 사용해도 결과적으로는 동일합니다. + */ + const articleEntity = await prismaClient.$transaction(async (tx) => { + const targetArticleEntity = await tx.article.findUnique({ + where: { + id: articleId, + }, + }); + + if (!targetArticleEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.ARTICLE_NOT_FOUND); + } + + if (targetArticleEntity.writerId !== requester.userId) { + throw new ForbiddenException('Forbidden', ExceptionMessage.FORBIDDEN); + } + + return await tx.article.update({ + where: { + id: articleId, + }, + data: { + title, + content, + image, + }, + }); + }); + + const article = new Article(articleEntity); + + const writerEntity = await prismaClient.user.findUnique({ + where: { + id: article.getWriterId(), + }, + }); + + const writer = new User(writerEntity); + + return { + id: article.getId(), + writer: { + id: writer.getId(), + nickname: writer.getNickname(), + }, + title: article.getTitle(), + content: article.getContent(), + image: article.getImage(), + createdAt: article.getCreatedAt(), + }; + } +} diff --git a/src/application/auth/AuthByGoogleHandler.ts b/src/application/auth/AuthByGoogleHandler.ts new file mode 100644 index 00000000..b387b533 --- /dev/null +++ b/src/application/auth/AuthByGoogleHandler.ts @@ -0,0 +1,88 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { AuthTokenManager } from '../../infra/AuthTokenManager'; +import { googleOAuthHelper } from '../../infra/GoogleOAuthAdapter'; + +import { InternalServerErrorException } from '../../exceptions/InternalServerErrorException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { User } from '../../domain/User'; + +export class AuthByGoogleHandler { + static async handle({ code } : { code: string }) { + const googleAccessToken = await googleOAuthHelper.getAccessToken(code); + const googleProfile = await googleOAuthHelper.getProfile(googleAccessToken); + + // 이미 존재하는 이메일인지 확인 + const existingUserEntity = await prismaClient.user.findUnique({ + where: { + email: googleProfile.email, + }, + }); + + // 이미 존재하는 경우, 로그인 처리 + if (existingUserEntity) { + const user = new User(existingUserEntity); + + const accessToken = AuthTokenManager.buildAccessToken({ userId: user.getId() }); + const refreshToken = AuthTokenManager.buildRefreshToken({ userId: user.getId() }); + await prismaClient.refreshToken.create({ + data: { + userId: user.getId(), + token: refreshToken, + }, + }); + + return { + accessToken, + refreshToken, + user: { + id: user.getId(), + email: user.getEmail(), + nickname: user.getNickname(), + image: user.getImage(), + createdAt: user.getCreatedAt(), + updatedAt: user.getUpdatedAt(), + }, + }; + } + + // 존재하지 않는 경우, 회원가입 처리 + if (!existingUserEntity) { + const createdUserEntity = await prismaClient.user.create({ + data: { + email: googleProfile.email, + nickname: googleProfile.nickname, + password: '', // 써드파티 유저의 경우 패스워드가 필요하지 않습니다. + image: googleProfile.image, + }, + }); + + const user = new User(createdUserEntity); + + const accessToken = AuthTokenManager.buildAccessToken({ userId: user.getId() }); + const refreshToken = AuthTokenManager.buildRefreshToken({ userId: user.getId() }); + await prismaClient.refreshToken.create({ + data: { + userId: user.getId(), + token: refreshToken, + }, + }); + + return { + accessToken, + refreshToken, + user: { + id: user.getId(), + email: user.getEmail(), + nickname: user.getNickname(), + image: user.getImage(), + createdAt: user.getCreatedAt(), + updatedAt: user.getUpdatedAt(), + }, + }; + } + + throw new InternalServerErrorException('Internal Server Error', ExceptionMessage.GOOGLE_LOGIN_FAILED); + } +} diff --git a/src/application/auth/RefreshTokenHandler.ts b/src/application/auth/RefreshTokenHandler.ts new file mode 100644 index 00000000..957f3aea --- /dev/null +++ b/src/application/auth/RefreshTokenHandler.ts @@ -0,0 +1,46 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { AuthTokenManager } from '../../infra/AuthTokenManager'; + +import { UnprocessableEntityException } from '../../exceptions/UnprocessableEntityException'; +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { User } from '../../domain/User'; + +export class RefreshTokenHandler { + static async handle({ refreshToken } : { refreshToken: string;}) { + if (!AuthTokenManager.isValidRefreshToken(refreshToken)) { + throw new UnprocessableEntityException('Unprocessable Entity', ExceptionMessage.INVALID_REFRESH_TOKEN); + } + + const requester = AuthTokenManager.getRequesterFromToken(`bearer ${refreshToken}`); + + const userEntity = await prismaClient.user.findUnique({ + where: { + id: requester.userId, + }, + }); + if (!userEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.USER_NOT_FOUND); + } + + const refreshTokenEntity = await prismaClient.refreshToken.findFirst({ + where: { + userId: requester.userId, + token: refreshToken, + }, + }); + if (!refreshTokenEntity) { + throw new UnprocessableEntityException('Unprocessable Entity', ExceptionMessage.INVALID_REFRESH_TOKEN); + } + + const user = new User(userEntity); + + return { + accessToken: AuthTokenManager.buildAccessToken({ + userId: user.getId(), + }), + }; + } +} diff --git a/src/application/auth/SignInLocalUserHandler.ts b/src/application/auth/SignInLocalUserHandler.ts new file mode 100644 index 00000000..8bd57a53 --- /dev/null +++ b/src/application/auth/SignInLocalUserHandler.ts @@ -0,0 +1,53 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { AuthTokenManager } from '../../infra/AuthTokenManager'; +import { UserPasswordBuilder } from '../../infra/UserPasswordBuilder'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { User } from '../../domain/User'; + +export class SignInLocalUserHandler { + static async handle({ email, password } : { email: string; password: string;}) { + const userEntity = await prismaClient.user.findUnique({ + where: { + email, + }, + }); + if (!userEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.USER_NOT_FOUND); + } + + const user = new User(userEntity); + + // 패스워드 일치 여부 확인 + if (!user.checkPassword(UserPasswordBuilder.hashPassword(password))) { + // 보안을 위해 비밀번호가 일치하지 않는 경우에도 USER_NOT_FOUND 에러메시지를 반환합니다. + throw new NotFoundException('Not Found', ExceptionMessage.USER_NOT_FOUND); + } + + // 액세스 토큰 및 리프레시 토큰 발급 + const accessToken = AuthTokenManager.buildAccessToken({ userId: user.getId() }); + const refreshToken = AuthTokenManager.buildRefreshToken({ userId: user.getId() }); + await prismaClient.refreshToken.create({ + data: { + userId: user.getId(), + token: refreshToken, + }, + }); + + return { + accessToken, + refreshToken, + user: { + id: user.getId(), + email: user.getEmail(), + nickname: user.getNickname(), + image: user.getImage(), + createdAt: user.getCreatedAt(), + updatedAt: user.getUpdatedAt(), + }, + }; + } +} diff --git a/src/application/auth/SignUpLocalUserHandler.ts b/src/application/auth/SignUpLocalUserHandler.ts new file mode 100644 index 00000000..20010ec5 --- /dev/null +++ b/src/application/auth/SignUpLocalUserHandler.ts @@ -0,0 +1,70 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { AuthTokenManager } from '../../infra/AuthTokenManager'; +import { UserPasswordBuilder } from '../../infra/UserPasswordBuilder'; + +import { UnprocessableEntityException } from '../../exceptions/UnprocessableEntityException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { User } from '../../domain/User'; + +type TSignUpLocalUser = { + email: string; + nickname: string; + password: string; + passwordConfirmation: string; +} + +export class SignUpLocalUserHandler { + static async handle({ email, nickname, password, passwordConfirmation } : TSignUpLocalUser) { + // 패스워드 및 패스워드 확인 일치 여부 확인 + if (password !== passwordConfirmation) { + throw new UnprocessableEntityException('Unprocessable Entity', + ExceptionMessage.PASSWORD_CONFIRMATION_NOT_MATCH, + ); + } + + // 이미 존재하는 이메일인지 확인 + const existingUser = await prismaClient.user.findUnique({ + where: { + email, + }, + }); + if (existingUser) { + throw new UnprocessableEntityException('Unprocessable Entity', ExceptionMessage.ALREADY_REGISTERED_EMAIL); + } + + const userEntity = await prismaClient.user.create({ + data: { + email, + nickname, + password: UserPasswordBuilder.hashPassword(password), + }, + }); + + const user = new User(userEntity); + + // 액세스 토큰 및 리프레시 토큰 발급 + const accessToken = AuthTokenManager.buildAccessToken({ userId: user.getId() }); + const refreshToken = AuthTokenManager.buildRefreshToken({ userId: user.getId() }); + await prismaClient.refreshToken.create({ + data: { + userId: user.getId(), + token: refreshToken, + }, + }); + + return { + accessToken, + refreshToken, + user: { + id: user.getId(), + email: user.getEmail(), + nickname: user.getNickname(), + image: user.getImage(), + createdAt: user.getCreatedAt(), + updatedAt: user.getUpdatedAt(), + }, + }; + } +} diff --git a/src/application/comment/DeleteCommentHandler.ts b/src/application/comment/DeleteCommentHandler.ts new file mode 100644 index 00000000..31916c05 --- /dev/null +++ b/src/application/comment/DeleteCommentHandler.ts @@ -0,0 +1,39 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ForbiddenException } from '../../exceptions/ForbiddenException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +type TDeleteCommentUser = { + userId : number; +} + +type TDeleteComment = { + commentId: number; +} + +export class DeleteCommentHandler { + static async handle(requester: TDeleteCommentUser, { commentId } : TDeleteComment) { + await prismaClient.$transaction(async (tx) => { + const targetCommentEntity = await tx.comment.findUnique({ + where: { + id: commentId, + }, + }); + + if (!targetCommentEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.COMMENT_NOT_FOUND); + } + + if (targetCommentEntity.writerId !== requester.userId) { + throw new ForbiddenException('Forbidden', ExceptionMessage.FORBIDDEN); + } + + return await tx.comment.delete({ + where: { + id: commentId, + }, + }); + }); + } +} diff --git a/src/application/comment/UpdateCommentHandler.ts b/src/application/comment/UpdateCommentHandler.ts new file mode 100644 index 00000000..84c7d16f --- /dev/null +++ b/src/application/comment/UpdateCommentHandler.ts @@ -0,0 +1,69 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ForbiddenException } from '../../exceptions/ForbiddenException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { Comment } from '../../domain/Comment'; +import { User } from '../../domain/User'; + +type TUpdateCommentUser = { + userId : number; +} + +type TUpdateComment = { + commentId: number | undefined; + content: string | undefined; +} + +export class UpdateCommentHandler { + static async handle(requester: TUpdateCommentUser, { commentId, content }: TUpdateComment) { + const commentEntity = await prismaClient.$transaction(async (tx) => { + const targetCommentEntity = await tx.comment.findUnique({ + where: { + id: commentId, + }, + }); + + if (!targetCommentEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.COMMENT_NOT_FOUND); + } + + if (targetCommentEntity.writerId !== requester.userId) { + throw new ForbiddenException('Forbidden', ExceptionMessage.FORBIDDEN); + } + + return await tx.comment.update({ + where: { + id: commentId, + }, + data: { + content, + }, + }); + }); + + const comment = new Comment(commentEntity); + + const writerEntity = await prismaClient.user.findUnique({ + where: { + id: comment.getWriterId(), + }, + }); + + const writer = new User(writerEntity); + + return { + id: comment.getId(), + writer: { + id: writer.getId(), + nickname: writer.getNickname(), + image: writer.getImage(), + }, + articleId: comment.getArticleId(), + productId: comment.getProductId(), + content: comment.getContent(), + createdAt: comment.getCreatedAt(), + }; + } +} diff --git a/src/application/product/CreateProductCommentHandler.ts b/src/application/product/CreateProductCommentHandler.ts new file mode 100644 index 00000000..038b5e12 --- /dev/null +++ b/src/application/product/CreateProductCommentHandler.ts @@ -0,0 +1,61 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { Comment } from '../../domain/Comment'; +import { User } from '../../domain/User'; +import { TProduct, TProductUser } from '@/types/product'; + +type TCreateProductComment = { + productId: number; + content: string; +} + +export class CreateProductCommentHandler { + static async handle( + requester : TProductUser, + { productId, content } : TCreateProductComment + ) { + const commentEntity = await prismaClient.$transaction(async (tx) => { + const targetProductEntity = await tx.product.findUnique({ + where: { + id: productId, + }, + }); + + if (!targetProductEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.PRODUCT_NOT_FOUND); + } + + return await tx.comment.create({ + data: { + productId: productId, + writerId: requester.userId, + content, + }, + }); + }); + + const comment = new Comment(commentEntity); + + const writerEntity = await prismaClient.user.findUnique({ + where: { + id: comment.getWriterId(), + }, + }); + + const writer = new User(writerEntity); + + return { + id: comment.getId(), + writer: { + id: writer.getId(), + nickname: writer.getNickname(), + image: writer.getImage(), + }, + content: comment.getContent(), + createdAt: comment.getCreatedAt(), + }; + } +} diff --git a/src/application/product/CreateProductHandler.ts b/src/application/product/CreateProductHandler.ts new file mode 100644 index 00000000..9cc516d2 --- /dev/null +++ b/src/application/product/CreateProductHandler.ts @@ -0,0 +1,40 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { Product } from '../../domain/Product'; +import { Prisma } from '@prisma/client'; +import { TProduct, TProductUser } from '@/types/product'; + +type TCreateProduct = { + name: string; + description: string; + price: number; + tags?: string[] | undefined; + images : string[] | Prisma.ProductCreateimagesInput | undefined; +} +export class CreateProductHandler { + static async handle(requester: TProductUser, { name, description, price, tags, images } : TCreateProduct) { + const productEntity = await prismaClient.product.create({ + data: { + ownerId: requester.userId, + name, + description, + price, + tags, + images, + }, + }); + + const product = new Product(productEntity); + + return { + id: product.getId(), + ownerId: product.getOwnerId(), + name: product.getName(), + description: product.getDescription(), + price: product.getPrice(), + tags: product.getTags(), + images: product.getImages(), + createdAt: product.getCreatedAt(), + }; + } +} diff --git a/src/application/product/CreateProductLikeHandler.ts b/src/application/product/CreateProductLikeHandler.ts new file mode 100644 index 00000000..1dd89f4c --- /dev/null +++ b/src/application/product/CreateProductLikeHandler.ts @@ -0,0 +1,61 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { Product } from '../../domain/Product'; +import { TProduct, TProductUser } from '@/types/product'; + +type TCreateProductLike = { + productId: number; +} + +export class CreateProductLikeHandler { + static async handle(requester: TProductUser, { productId } : TCreateProductLike) { + const productEntity = await prismaClient.$transaction(async (tx) => { + const targetProductEntity = await tx.product.findUnique({ + where: { + id: productId, + }, + }); + + if (!targetProductEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.PRODUCT_NOT_FOUND); + } + + const likeEntity = await tx.like.findUnique({ + where: { + userId_productId: { + userId: requester.userId, + productId, + }, + }, + }); + + if (!likeEntity) { + await tx.like.create({ + data: { + productId: Number(productId), + userId: requester.userId, + }, + }); + } + + return targetProductEntity; + }); + + const product = new Product(productEntity); + + return { + id: product.getId(), + ownerId: product.getOwnerId(), + name: product.getName(), + description: product.getDescription(), + price: product.getPrice(), + tags: product.getTags(), + images: product.getImages(), + createdAt: product.getCreatedAt(), + isFavorite: true, + }; + } +} diff --git a/src/application/product/DeleteProductHandler.ts b/src/application/product/DeleteProductHandler.ts new file mode 100644 index 00000000..1ca913e2 --- /dev/null +++ b/src/application/product/DeleteProductHandler.ts @@ -0,0 +1,37 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ForbiddenException } from '../../exceptions/ForbiddenException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; +import { TProduct, TProductUser } from '@/types/product'; + +type TDeleteProduct = { + productId: number; +} + + +export class DeleteProductHandler { + static async handle(requester : TProductUser, { productId } : TDeleteProduct) { + await prismaClient.$transaction(async (tx) => { + const targetProductEntity = await tx.product.findUnique({ + where: { + id: Number(productId), + }, + }); + + if (!targetProductEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.PRODUCT_NOT_FOUND); + } + + if (targetProductEntity.ownerId !== requester.userId) { + throw new ForbiddenException('Forbidden', ExceptionMessage.FORBIDDEN); + } + + return await tx.product.delete({ + where: { + id: Number(productId), + }, + }); + }); + } +} diff --git a/src/application/product/DeleteProductLikeHandler.ts b/src/application/product/DeleteProductLikeHandler.ts new file mode 100644 index 00000000..bfb0da8e --- /dev/null +++ b/src/application/product/DeleteProductLikeHandler.ts @@ -0,0 +1,63 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { Product } from '../../domain/Product'; +import { TProduct, TProductUser } from '@/types/product'; + +type TDeleteProductLike = { + productId: number; +} + +export class DeleteProductLikeHandler { + static async handle(requester : TProductUser, { productId }: TDeleteProductLike) { + const productEntity = await prismaClient.$transaction(async (tx) => { + const targetProductEntity = await tx.product.findUnique({ + where: { + id: productId, + }, + }); + + if (!targetProductEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.PRODUCT_NOT_FOUND); + } + + const likeEntity = await tx.like.findUnique({ + where: { + userId_productId: { + userId: requester.userId, + productId, + }, + }, + }); + + if (likeEntity) { + await tx.like.delete({ + where: { + userId_productId: { + userId: requester.userId, + productId, + }, + }, + }); + } + + return targetProductEntity; + }); + + const product = new Product(productEntity); + + return { + id: product.getId(), + ownerId: product.getOwnerId(), + name: product.getName(), + description: product.getDescription(), + price: product.getPrice(), + tags: product.getTags(), + images: product.getImages(), + createdAt: product.getCreatedAt(), + isFavorite: false, + }; + } +} diff --git a/src/application/product/GetProductCommentListHandler.ts b/src/application/product/GetProductCommentListHandler.ts new file mode 100644 index 00000000..fe909a15 --- /dev/null +++ b/src/application/product/GetProductCommentListHandler.ts @@ -0,0 +1,79 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { Comment } from '../../domain/Comment'; +import { User } from '../../domain/User'; +import { Prisma } from '@prisma/client'; + +type TGetProductCommentList = { + productId: number; + cursor?: number | undefined + limit: number | undefined; +} + +export class GetProductCommentListHandler { + static async handle({ productId, cursor, limit } : TGetProductCommentList) { + const commentEntities = await prismaClient.$transaction(async (tx) => { + const targetProductEntity = await tx.product.findUnique({ + where: { + id: Number(productId), + }, + }); + + if (!targetProductEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.PRODUCT_NOT_FOUND); + } + + return await tx.comment.findMany({ + cursor: cursor + ? { + id: cursor, + } + : undefined, + take: Number(limit) + 1, + where: { + productId: Number(productId), + }, + }); + }); + + const comments = commentEntities.map((commentEntity) => new Comment(commentEntity)); + + const writerEntities = await prismaClient.user.findMany({ + where: { + id: { + in: Array.from(new Set(comments.map((comment) => comment.getWriterId()))), + }, + }, + }); + + const writers = writerEntities.map((writerEntity) => new User(writerEntity)); + const hasNext = comments.length === Number(limit) + 1; + + return { + list: comments.slice(0, limit).map((comment) => { + const writer = writers.find((writer) => writer.getId() === comment.getWriterId()); + + if(!writer) { + throw new Error('Writer not found'); + } + + return { + id: comment.getId(), + writer: { + id: writer.getId(), + nickname: writer.getNickname(), + image: writer.getImage(), + }, + productId: comment.getProductId(), + content: comment.getContent(), + createdAt: comment.getCreatedAt(), + updatedAt: comment.getUpdatedAt(), + }; + }), + nextCursor: hasNext ? comments[comments.length - 1].getId() : null, + }; + } +} diff --git a/src/application/product/GetProductHandler.ts b/src/application/product/GetProductHandler.ts new file mode 100644 index 00000000..56f2ffdc --- /dev/null +++ b/src/application/product/GetProductHandler.ts @@ -0,0 +1,49 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { Product } from '../../domain/Product'; +import { TProduct, TProductUser } from '@/types/product'; + +type TGetProduct = { + productId: number; +} + +export class GetProductHandler { + static async handle(requester : TProductUser, { productId } : TGetProduct) { + const productEntity = await prismaClient.product.findUnique({ + where: { + id: Number(productId), + }, + include: { + likes: { + select: { + // 좋아요의 id, userId만 필요함 + id: true, + userId: true, + }, + }, + }, + }); + + if (!productEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.PRODUCT_NOT_FOUND); + } + + const product = new Product(productEntity); + + return { + id: product.getId(), + ownerId: product.getOwnerId(), + name: product.getName(), + description: product.getDescription(), + price: product.getPrice(), + tags: product.getTags(), + images: product.getImages(), + createdAt: product.getCreatedAt(), + favoriteCount: product.getFavoriteCount(), + isFavorite: product.getIsFavorite(requester.userId), + }; + } +} diff --git a/src/application/product/GetProductListHandler.ts b/src/application/product/GetProductListHandler.ts new file mode 100644 index 00000000..940f84ee --- /dev/null +++ b/src/application/product/GetProductListHandler.ts @@ -0,0 +1,85 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { Product } from '../../domain/Product'; +import { Like } from '../../domain/Like'; +import { Prisma } from '@prisma/client'; + +type TGetProductListUser = { + userId : number; +} + +type TGetProductList = { + page: number; + pageSize: number; + orderBy: 'favorite' | 'recent'; + keyword: string | undefined; +} + +export class GetProductListHandler { + static async handle(requester : TGetProductListUser, { page, pageSize, orderBy, keyword } : TGetProductList) { + const whereClause = keyword + ? { + OR: [ + { + name: { + contains: keyword, + }, + }, + { + description: { + contains: keyword, + }, + }, + ], + } + : undefined; + + const matchedProductCount = await prismaClient.product.count({ + where: whereClause, + }); + + const orderByOption = + orderBy === 'favorite' + ? { _count: { likes: 'desc' as Prisma.SortOrder } } + : { createdAt: 'desc' as Prisma.SortOrder }; + + const productEntities = await prismaClient.product.findMany({ + skip: pageSize * (page - 1), + take: pageSize, + where: whereClause, + orderBy: orderByOption, + include: { + _count: { + select: { likes: true }, // 각 Product의 전체 Like 개수 + }, + likes: { + select: { + // 좋아요의 id, userId만 필요함 + id: true, + userId: true, + }, + }, + }, + }); + + const products = productEntities.map( + (productEntity) => new Product(productEntity) + ); + + return { + totalCount: matchedProductCount, + list: products.slice(0, pageSize).map((product) => ({ + id: product.getId(), + ownerId: product.getOwnerId(), + name: product.getName(), + description: product.getDescription(), + price: product.getPrice(), + tags: product.getTags(), + images: product.getImages(), + createdAt: product.getCreatedAt(), + favoriteCount: product.getFavoriteCount(), + isFavorite: product.getIsFavorite(requester.userId), + })), + }; + } +} diff --git a/src/application/product/UpdateProductHandler.ts b/src/application/product/UpdateProductHandler.ts new file mode 100644 index 00000000..fddf09f4 --- /dev/null +++ b/src/application/product/UpdateProductHandler.ts @@ -0,0 +1,63 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ForbiddenException } from '../../exceptions/ForbiddenException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { Product } from '../../domain/Product'; +import { TProductUser } from '@/types/product'; + +type TUpdateProduct = { + productId: number; + name: string | undefined; + description: string | undefined; + price: number | undefined; + tags: string[] | undefined; + images: string[] | undefined; +} + +export class UpdateProductHandler { + static async handle(requester: TProductUser, { productId, name, description, price, tags, images } : TUpdateProduct) { + const productEntity = await prismaClient.$transaction(async (tx) => { + const targetProductEntity = await tx.product.findUnique({ + where: { + id: Number(productId), + }, + }); + + if (!targetProductEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.PRODUCT_NOT_FOUND); + } + + if (targetProductEntity.ownerId !== requester.userId) { + throw new ForbiddenException('Forbidden', ExceptionMessage.FORBIDDEN); + } + + return await tx.product.update({ + where: { + id: Number(productId), + }, + data: { + name, + description, + price, + tags, + images, + }, + }); + }); + + const product = new Product(productEntity); + + return { + id: product.getId(), + ownerId: product.getOwnerId(), + name: product.getName(), + description: product.getDescription(), + price: product.getPrice(), + tags: product.getTags(), + images: product.getImages(), + createdAt: product.getCreatedAt(), + }; + } +} diff --git a/src/application/user/GetUserFavoriteListHandler.ts b/src/application/user/GetUserFavoriteListHandler.ts new file mode 100644 index 00000000..8e6ba772 --- /dev/null +++ b/src/application/user/GetUserFavoriteListHandler.ts @@ -0,0 +1,78 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { Product } from '../../domain/Product'; + +type TGetUserFavoriteListUser = { + userId: number; +} + +type TGetUserFavoriteList = { + page: number; + pageSize: number; + keyword: string | undefined; +} + +export class GetUserFavoriteListHandler { + static async handle(requester: TGetUserFavoriteListUser, { page, pageSize, keyword }: TGetUserFavoriteList) { + const userEntity = await prismaClient.user.findUnique({ + where: { + id: requester.userId, + }, + }); + + if (!userEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.USER_NOT_FOUND); + } + + const favoriteProductCount = await prismaClient.product.count({ + where: { + likes: { + some: { + userId: requester.userId, + }, + }, + name: { + contains: keyword, + }, + }, + skip: (page - 1) * pageSize, + take: pageSize, + }); + + const favoriteProductsEntities = await prismaClient.product.findMany({ + where: { + likes: { + some: { + userId: requester.userId, + }, + }, + name: { + contains: keyword, + }, + }, + skip: (page - 1) * pageSize, + take: pageSize, + }); + + const favoriteProducts = favoriteProductsEntities.map( + (favoriteProductEntity) => new Product(favoriteProductEntity), + ); + + return { + totalCount: favoriteProductCount, + list: favoriteProducts.map((product) => ({ + id: product.getId(), + ownerId: product.getOwnerId(), + name: product.getName(), + description: product.getDescription(), + price: product.getPrice(), + tags: product.getTags(), + images: product.getImages(), + createdAt: product.getCreatedAt(), + })), + }; + } +} diff --git a/src/application/user/GetUserProductListHandler.ts b/src/application/user/GetUserProductListHandler.ts new file mode 100644 index 00000000..467fbbdd --- /dev/null +++ b/src/application/user/GetUserProductListHandler.ts @@ -0,0 +1,65 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { Product } from '../../domain/Product'; + +type TGetUserProductListUser = { + userId : number; +} + +type TGetUserProductList = { + page: number | undefined; + pageSize: number | undefined; + keyword?: string; +} +export class GetUserProductListHandler { + static async handle(requester: TGetUserProductListUser, { page, pageSize, keyword } : TGetUserProductList) { + const userEntity = await prismaClient.user.findUnique({ + where: { + id: requester.userId, + }, + }); + if (!userEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.USER_NOT_FOUND); + } + + const productCount = await prismaClient.product.count({ + where: { + ownerId: requester.userId, + name: { + contains: keyword, + }, + }, + }); + + const productEntities = await prismaClient.product.findMany({ + where: { + ownerId: requester.userId, + name: { + contains: keyword, + }, + }, + skip: (Number(page) - 1) * Number(pageSize), + take: pageSize, + }); + + const products = productEntities.map((productEntity) => new Product(productEntity)); + + return { + totalCount: productCount, + list: products.map((product) => ({ + id: product.getId(), + ownerId: product.getOwnerId(), + name: product.getName(), + description: product.getDescription(), + price: product.getPrice(), + tags: product.getTags(), + images: product.getImages(), + createdAt: product.getCreatedAt(), + updatedAt: product.getUpdatedAt(), + })), + }; + } +} diff --git a/src/application/user/GetUserProfileHandler.ts b/src/application/user/GetUserProfileHandler.ts new file mode 100644 index 00000000..177df1bb --- /dev/null +++ b/src/application/user/GetUserProfileHandler.ts @@ -0,0 +1,34 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { User } from '../../domain/User'; + +type TGetUserProfile = { + userId : number; +} + +export class GetUserProfileHandler { + static async handle(requester : TGetUserProfile) { + const userEntity = await prismaClient.user.findUnique({ + where: { + id: requester.userId, + }, + }); + if (!userEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.USER_NOT_FOUND); + } + + const user = new User(userEntity); + + return { + id: user.getId(), + email: user.getEmail(), + nickname: user.getNickname(), + image: user.getImage(), + createdAt: user.getCreatedAt(), + updatedAt: user.getUpdatedAt(), + }; + } +} diff --git a/src/application/user/UpdateUserPasswordHandler.ts b/src/application/user/UpdateUserPasswordHandler.ts new file mode 100644 index 00000000..d95e303d --- /dev/null +++ b/src/application/user/UpdateUserPasswordHandler.ts @@ -0,0 +1,70 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { UserPasswordBuilder } from '../../infra/UserPasswordBuilder'; + +import { UnprocessableEntityException } from '../../exceptions/UnprocessableEntityException'; +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { User } from '../../domain/User'; + +type TUpdateUserPasswordUser = { + userId : number; +} + +type TUpdateUserPassword = { + password: string; + passwordConfirmation: string; + currentPassword: string; +} + + +export class UpdateUserPasswordHandler { + static async handle(requester: TUpdateUserPasswordUser, { password, passwordConfirmation, currentPassword } : TUpdateUserPassword) { + // 패스워드와 패스워드 확인이 일치하는지 검증 + if (password !== passwordConfirmation) { + throw new UnprocessableEntityException('Unprocessable Entity', + ExceptionMessage.PASSWORD_CONFIRMATION_NOT_MATCH, + ); + } + + const userEntity = await prismaClient.user.findUnique({ + where: { + id: requester.userId, + }, + }); + if (!userEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.USER_NOT_FOUND); + } + + const user = new User(userEntity); + + // 현재 패스워드가 일치하는지 검증 + if (!user.checkPassword(UserPasswordBuilder.hashPassword(currentPassword))) { + throw new UnprocessableEntityException('Unprocessable Entity', ExceptionMessage.CURRENT_PASSWORD_NOT_MATCH); + } + + // 비밀번호 변경 진행 + const hashedPassword = UserPasswordBuilder.hashPassword(password); + user.setPassword(hashedPassword); + await prismaClient.user.update({ + where: { + id: user.getId(), + }, + data: { + password: hashedPassword, + }, + }); + + return { + user: { + id: user.getId(), + email: user.getEmail(), + nickname: user.getNickname(), + image: user.getImage(), + createdAt: user.getCreatedAt(), + updatedAt: user.getUpdatedAt(), + }, + }; + } +} diff --git a/src/application/user/UpdateUserProfileHandler.ts b/src/application/user/UpdateUserProfileHandler.ts new file mode 100644 index 00000000..5870dec7 --- /dev/null +++ b/src/application/user/UpdateUserProfileHandler.ts @@ -0,0 +1,45 @@ +import { prismaClient } from '../../infra/prismaClient'; + +import { NotFoundException } from '../../exceptions/NotFoundException'; +import { ExceptionMessage } from '../../constant/ExceptionMessage'; + +import { User } from '../../domain/User'; + +type TUpdateUserProfileUser = { + userId: number; +} + +export class UpdateUserProfileHandler { + static async handle(requester: TUpdateUserProfileUser, { image }: { image: string | null }) { + const userEntity = await prismaClient.user.findUnique({ + where: { + id: requester.userId, + }, + }); + if (!userEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.USER_NOT_FOUND); + } + + const user = new User(userEntity); + user.setImage(image); + await prismaClient.user.update({ + where: { + id: user.getId(), + }, + data: { + image: user.getImage(), + }, + }); + + return { + user: { + id: user.getId(), + email: user.getEmail(), + nickname: user.getNickname(), + image: user.getImage(), + createdAt: user.getCreatedAt(), + updatedAt: user.getUpdatedAt(), + }, + }; + } +} diff --git a/src/constant/ExceptionMessage.js b/src/constant/ExceptionMessage.js new file mode 100644 index 00000000..3aa8cd29 --- /dev/null +++ b/src/constant/ExceptionMessage.js @@ -0,0 +1,22 @@ +/** + * [에러 메시지 상수] + * + * 에러 메시지가 반복적으로 사용되는 경우, 상수로 관리하는 것이 효율적입니다. + * + * 여러 장점들: + * - 오타 방지 + * - 추후 에러메시지 변경에 유리 + * - ... + */ +export const ExceptionMessage = { + ARTICLE_NOT_FOUND: '게시글을 찾을 수 없습니다', + PRODUCT_NOT_FOUND: '상품을 찾을 수 없습니다', + COMMENT_NOT_FOUND: '댓글을 찾을 수 없습니다', + USER_NOT_FOUND: '사용자를 찾을 수 없습니다', + PASSWORD_CONFIRMATION_NOT_MATCH: '비밀번호 확인이 일치하지 않습니다', + CURRENT_PASSWORD_NOT_MATCH: '현재 비밀번호가 일치하지 않습니다', + ALREADY_REGISTERED_EMAIL: '이미 등록된 이메일입니다', + FORBIDDEN: '접근이 금지되었습니다', + INVALID_REFRESH_TOKEN: '유효하지 않은 리프레시 토큰입니다', + GOOGLE_LOGIN_FAILED: '구글 로그인에 실패하였습니다', +}; diff --git a/src/domain/Article.js b/src/domain/Article.js new file mode 100644 index 00000000..8ded4a65 --- /dev/null +++ b/src/domain/Article.js @@ -0,0 +1,71 @@ +export class Article { + /** ID */ + _id; + + /** 작성자 ID */ + _writerId; + + /** 제목 */ + _title; + + /** 내용 */ + _content; + + /** 이미지 */ + _image; + + /** 작성시각 */ + _createdAt; + + /** 마지막 수정시각 */ + _updatedAt; + + constructor(param) { + this._id = param.id; + this._writerId = param.writerId; + this._title = param.title; + this._content = param.content; + this._image = param.image; + this._createdAt = param.createdAt; + this._updatedAt = param.updatedAt; + this._likes = params.likes; + } + + getId() { + return this._id; + } + + getWriterId() { + return this._writerId; + } + + getTitle() { + return this._title; + } + + getContent() { + return this._content; + } + + getImage() { + return this._image; + } + + getCreatedAt() { + return this._createdAt; + } + + getUpdatedAt() { + return this._updatedAt; + } + + getIsFavorite(userId) { + if (!userId) return false; + + return this._likes.some((like) => like.userId === userId); + } + + getFavoriteCount() { + return this._likes.length; + } +} diff --git a/src/domain/Comment.js b/src/domain/Comment.js new file mode 100644 index 00000000..5642ab8b --- /dev/null +++ b/src/domain/Comment.js @@ -0,0 +1,60 @@ +export class Comment { + /** ID */ + _id; + + /** 작성자 ID */ + _writerId; + + /** 게시글 ID */ + _articleId; + + /** 상품 ID */ + _productId; + + /** 내용 */ + _content; + + /** 작성시각 */ + _createdAt; + + /** 마지막 수정시각 */ + _updatedAt; + + constructor(param) { + this._id = param.id; + this._writerId = param.writerId; + this._articleId = param.articleId; + this._productId = param.productId; + this._content = param.content; + this._createdAt = param.createdAt; + this._updatedAt = param.updatedAt; + } + + getId() { + return this._id; + } + + getWriterId() { + return this._writerId; + } + + getArticleId() { + return this._articleId; + } + + getProductId() { + return this._productId; + } + + getContent() { + return this._content; + } + + getCreatedAt() { + return this._createdAt; + } + + getUpdatedAt() { + return this._updatedAt; + } +} diff --git a/src/domain/Like.js b/src/domain/Like.js new file mode 100644 index 00000000..b0566712 --- /dev/null +++ b/src/domain/Like.js @@ -0,0 +1,44 @@ +export class Like { + /** ID */ + _id; + + /** 사용자 ID */ + _userId; + + /** 상품 ID */ + _productId; + + /** 게시글 ID */ + _articleId; + + /** 생성시각 */ + _createdAt; + + constructor(param) { + this._id = param.id; + this._userId = param.userId; + this._productId = param.productId; + this._articleId = param.articleId; + this._createdAt = param.createdAt; + } + + getId() { + return this._id; + } + + getUserId() { + return this._userId; + } + + getProductId() { + return this._productId; + } + + getArticleId() { + return this._articleId; + } + + getCreatedAt() { + return this._createdAt; + } +} diff --git a/src/domain/Product.js b/src/domain/Product.js new file mode 100644 index 00000000..d84a3d39 --- /dev/null +++ b/src/domain/Product.js @@ -0,0 +1,90 @@ +export class Product { + /** ID */ + _id; + + /** 작성자 ID */ + _ownerId; + + /** 상품명 */ + _name; + + /** 상품 설명 */ + _description; + + /** 판매 가격 */ + _price; + + /** 해시 태그 목록 */ + _tags; + + /** 이미지 목록 */ + _images; + + /** 생성시각 */ + _createdAt; + + /** 마지막 수정시각 */ + _updatedAt; + + /** 좋아요 목록 */ + _likes; + + constructor(param) { + this._id = param.id; + this._ownerId = param.ownerId; + this._name = param.name; + this._description = param.description; + this._price = param.price; + this._tags = Array.from(param.tags); // 깊은 복사를 통해, 외부의 배열을 통해 내부 배열을 변경할 수 없도록 합니다. + this._images = Array.from(param.images); + this._createdAt = param.createdAt; + this._updatedAt = param.updatedAt; + this._likes = param.likes ?? []; + } + + getId() { + return this._id; + } + + getOwnerId() { + return this._ownerId; + } + + getName() { + return this._name; + } + + getDescription() { + return this._description; + } + + getPrice() { + return this._price; + } + + getTags() { + return Array.from(this._tags); // 깊은 복사를 통해, 반환된 배열을 통해 내부 배열을 변경할 수 없도록 합니다. + } + + getImages() { + return Array.from(this._images); + } + + getCreatedAt() { + return this._createdAt; + } + + getUpdatedAt() { + return this._updatedAt; + } + + getIsFavorite(userId) { + if (!userId) return false; + + return this._likes.some((like) => like.userId === userId); + } + + getFavoriteCount() { + return this._likes.length; + } +} diff --git a/src/domain/User.js b/src/domain/User.js new file mode 100644 index 00000000..85357607 --- /dev/null +++ b/src/domain/User.js @@ -0,0 +1,68 @@ +export class User { + /** ID */ + _id; + + /** 이메일 */ + _email; + + /** 비밀번호 */ + _password; + + /** 닉네임 */ + _nickname; + + /** 이미지 */ + _image; + + /** 생성시각 */ + _createdAt; + + /** 마지막 수정시각 */ + _updatedAt; + + constructor(param) { + this._id = param.id; + this._email = param.email; + this._password = param.password; + this._nickname = param.nickname; + this._image = param.image; + this._createdAt = param.createdAt; + this._updatedAt = param.updatedAt; + } + + getId() { + return this._id; + } + + getEmail() { + return this._email; + } + + getNickname() { + return this._nickname; + } + + getImage() { + return this._image; + } + + getCreatedAt() { + return this._createdAt; + } + + getUpdatedAt() { + return this._updatedAt; + } + + setImage(image) { + this._image = image; + } + + setPassword(password) { + this._password = password; + } + + checkPassword(password) { + return this._password === password; + } +} diff --git a/src/exceptions/BadRequestException.js b/src/exceptions/BadRequestException.js new file mode 100644 index 00000000..5c27518b --- /dev/null +++ b/src/exceptions/BadRequestException.js @@ -0,0 +1,11 @@ +import { HttpException } from './HttpException.js'; + +export class BadRequestException extends HttpException { + constructor(name, message) { + super({ + status: 400, + name, + message, + }); + } +} diff --git a/src/exceptions/ForbiddenException.js b/src/exceptions/ForbiddenException.js new file mode 100644 index 00000000..d0508962 --- /dev/null +++ b/src/exceptions/ForbiddenException.js @@ -0,0 +1,11 @@ +import { HttpException } from './HttpException.js'; + +export class ForbiddenException extends HttpException { + constructor(name, message) { + super({ + status: 403, + name, + message, + }); + } +} diff --git a/src/exceptions/HttpException.js b/src/exceptions/HttpException.js new file mode 100644 index 00000000..47e75836 --- /dev/null +++ b/src/exceptions/HttpException.js @@ -0,0 +1,19 @@ +/** + * [일관성있는 에러처리를 위한 HttpException 클래스] + * + * 해당 서버에서 발생하는 모든 에러는 HttpException 으로 변환되어 응답되어야 합니다. + * 자세한 내용은 아래 코드를 참고하세요. + * + * @see asyncErrorHandler + */ +export class HttpException extends Error { + status; + name; + + constructor(param) { + const { status, name, message } = param; + super(message); + this.status = status; + this.name = name; + } +} diff --git a/src/exceptions/InternalServerErrorException.js b/src/exceptions/InternalServerErrorException.js new file mode 100644 index 00000000..ee3ca246 --- /dev/null +++ b/src/exceptions/InternalServerErrorException.js @@ -0,0 +1,11 @@ +import { HttpException } from './HttpException.js'; + +export class InternalServerErrorException extends HttpException { + constructor(name, message) { + super({ + status: 500, + name, + message, + }); + } +} diff --git a/src/exceptions/NotFoundException.js b/src/exceptions/NotFoundException.js new file mode 100644 index 00000000..29c62c88 --- /dev/null +++ b/src/exceptions/NotFoundException.js @@ -0,0 +1,11 @@ +import { HttpException } from './HttpException.js'; + +export class NotFoundException extends HttpException { + constructor(name, message) { + super({ + status: 404, + name, + message, + }); + } +} diff --git a/src/exceptions/UnprocessableEntityException.js b/src/exceptions/UnprocessableEntityException.js new file mode 100644 index 00000000..bbd40adc --- /dev/null +++ b/src/exceptions/UnprocessableEntityException.js @@ -0,0 +1,11 @@ +import { HttpException } from './HttpException.js'; + +export class UnprocessableEntityException extends HttpException { + constructor(name, message) { + super({ + status: 422, + name, + message, + }); + } +} diff --git a/src/infra/AuthTokenManager.ts b/src/infra/AuthTokenManager.ts new file mode 100644 index 00000000..5e79a7ce --- /dev/null +++ b/src/infra/AuthTokenManager.ts @@ -0,0 +1,92 @@ +import jwt from 'jsonwebtoken'; + +type TisValidAccessToken = { + accessToken: string | null; +} + +export class AuthTokenManager { + /** + * 현재 시각으로부터 1시간동안 유효한 액세스 토큰을 생성합니다. + */ + static buildAccessToken(payload: any) { + return jwt.sign( + { + user: { + id: payload.userId, + }, + }, + process.env.JWT_ACCESS_TOKEN_SECRET!, + { + expiresIn: '1h', + }, + ); + } + + /** + * 주어진 액세스 토큰이 유효한지 검증합니다. + */ + static isValidAccessToken(accessToken : string) { + try { + jwt.verify(accessToken, process.env.JWT_ACCESS_TOKEN_SECRET!); + + return true; + } catch (e) { + return false; + } + } + + /** + * 현재 시각으로부터 14일동안 유효한 리프레시 토큰을 생성합니다. + */ + static buildRefreshToken(payload: any) { + return jwt.sign( + { + user: { + id: payload.userId, + }, + }, + process.env.JWT_REFRESH_TOKEN_SECRET!, + { + expiresIn: '14d', + }, + ); + } + + static isValidRefreshToken(refreshToken: string) { + try { + jwt.verify(refreshToken, process.env.JWT_REFRESH_TOKEN_SECRET!); + + return true; + } catch (e) { + return false; + } + } + + /** + * 액세스 토큰 또는 리프래시 토큰으로부터 요청자 정보를 추출합니다. + */ + static getRequesterFromToken(authorizationHeaderValue: any) { + const jwtToken = authorizationHeaderValue.split(' ')[1]; // "bearer JWT_TOKEN" 형태로 전달받음 + + const jwtPayload = jwt.decode(jwtToken); + + + if (!jwtPayload || typeof jwtPayload === 'string') { + throw new Error('Invalid JWT payload'); + } + + return { + userId: jwtPayload.user.id, + }; + } + + static getRequesterFromTokenOrDefault(authorizationHeaderValue: string) { + try { + return this.getRequesterFromToken(authorizationHeaderValue); + } catch (e) { + return { + userId: -1, // GUEST + }; + } + } +} diff --git a/src/infra/GoogleOAuthAdapter.ts b/src/infra/GoogleOAuthAdapter.ts new file mode 100644 index 00000000..986144db --- /dev/null +++ b/src/infra/GoogleOAuthAdapter.ts @@ -0,0 +1,60 @@ +import * as Axios from 'axios'; + + +class GoogleOAuthAdapter { + _httpClient = Axios.default.create(); + + + /** + * Google Consent Screen (구글의 로그인 페이지)으로 가는 URI를 리턴합니다. + * 이 URI로 클라이언트 웹 브라우저를 리다이렉트 시키는 용도입니다. + */ + generateAuthURI() { + const searchParams = new URLSearchParams({ + client_id: process.env.GOOGLE_CLIENT_ID!, + redirect_uri: process.env.GOOGLE_REDIRECT_URI!, + response_type: 'code', + scope: 'email profile', + access_type: 'offline', + } ) + const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${searchParams.toString()}` + return authUrl; + } + + /** + * 구글 OAuth AccessToken 발급을 요청합니다. + */ + async getAccessToken(code : string) { + const response = await this._httpClient.post('https://oauth2.googleapis.com/token', { + code, + client_id: process.env.GOOGLE_CLIENT_ID, + client_secret: process.env.GOOGLE_CLIENT_SECRET, + redirect_uri: process.env.GOOGLE_REDIRECT_URI, + grant_type: 'authorization_code', + }); + + return response.data.access_token; + } + + /** + * 구글 OAuth AccessToken을 이용해 사용자 프로필 정보를 가져옵니다. + */ + async getProfile(accessToken : string) { + const response = await this._httpClient.get( + 'https://www.googleapis.com/oauth2/v2/userinfo', + { + params: { access_token: accessToken }, + }, + ); + + const { email, name, picture } = response.data; + + return { + email, + nickname: name, + image: picture || null, + }; + } +} + +export const googleOAuthHelper = new GoogleOAuthAdapter(); diff --git a/src/infra/UserPasswordBuilder.ts b/src/infra/UserPasswordBuilder.ts new file mode 100644 index 00000000..c2cdea92 --- /dev/null +++ b/src/infra/UserPasswordBuilder.ts @@ -0,0 +1,10 @@ +import crypto from 'crypto'; + +export class UserPasswordBuilder { + /** + * 보안상의 이유로 사용자 비밀번호를 해싱합니다. + */ + static hashPassword(password : string) { + return crypto.createHash('sha512').update(password).digest('base64'); + } +} diff --git a/src/infra/prismaClient.ts b/src/infra/prismaClient.ts new file mode 100644 index 00000000..5e41d6fe --- /dev/null +++ b/src/infra/prismaClient.ts @@ -0,0 +1,3 @@ +import { PrismaClient } from '@prisma/client'; + +export const prismaClient = new PrismaClient(); diff --git a/src/interface/ArticleRouter.ts b/src/interface/ArticleRouter.ts new file mode 100644 index 00000000..d25aecb0 --- /dev/null +++ b/src/interface/ArticleRouter.ts @@ -0,0 +1,216 @@ +import express, { Request, Response } from 'express'; +import { create } from 'superstruct'; +import { AuthN } from './utils/AuthN'; + +import { AuthTokenManager } from '../infra/AuthTokenManager'; + +import { asyncErrorHandler } from './utils/asyncErrorHandler'; +import { CreateArticleRequestStruct } from './structs/article/CreateArticleRequestStruct'; +import { UpdateArticleRequestStruct } from './structs/article/UpdateArticleRequestStruct'; +import { GetArticleListRequestStruct } from './structs/article/GetArticleListRequestStruct'; +import { CreateCommentRequestStruct } from './structs/comment/CreateCommentRequestStruct'; +import { GetCommentListRequestStruct } from './structs/comment/GetCommentListRequestStruct'; + +import { CreateArticleHandler } from '../application/article/CreateArticleHandler'; +import { GetArticleHandler } from '../application/article/GetArticleHandler'; +import { UpdateArticleHandler } from '../application/article/UpdateArticleHandler'; +import { DeleteArticleHandler } from '../application/article/DeleteArticleHandler'; +import { GetArticleListHandler } from '../application/article/GetArticleListHandler'; +import { CreateArticleCommentHandler } from '../application/article/CreateArticleCommentHandler'; +import { GetArticleCommentListHandler } from '../application/article/GetArticleCommentListHandler'; +import { CreateArticleLikeHandler } from '../application/article/CreateArticleLikeHandler'; +import { DeleteArticleLikeHandler } from '../application/article/DeleteArticleLikeHandler'; + +import { TArticle } from '@/types/article'; + +export const ArticleRouter = express.Router(); + +type ArticleRouterQuery = { + cursor?: string; + limit?: string | undefined; + orderBy: "recent" | "favorite"; + keyword: string | undefined; +} + +// 게시글 등록 api +ArticleRouter.post( + '/', + AuthN(), + asyncErrorHandler(async (req:Request<{}, {}, TArticle>, res:Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + /** + * [API 요청 유효성 검사] + * + * assert 메서드는 유효성 검사만 시도하는데 비해, + * create 메서드는 데이터를 전처리하고, 유효성 검사를 같이 시도합니다. + * + * 전처리를 하는 이유는 아래와 같이 다양합니다. + * - 기본값을 설정하기 위해 @see GetArticleListRequestStruct + * - 데이터를 변환하기 위해 + * 1. 문자열 앞뒤에 있는 공백 제거 @see CreateArticleRequestStruct + * 2. 문자열로 이루어진 숫자 -> 숫자 @see GetArticleListRequestStruct + * ... + */ + const { title, content, image } = create(req.body, CreateArticleRequestStruct); + + const articleView = await CreateArticleHandler.handle(requester, { + title, + content, + image, + }); + + return res.status(201).send(articleView); + }), +); + +// 게시글 조회 api +ArticleRouter.get( + '/:articleId', + asyncErrorHandler(async (req: Request< {articleId: number} >, res: Response) => { + const requester = AuthTokenManager.getRequesterFromTokenOrDefault( + req.headers.authorization!, + ); + + const articleId = Number(req.params.articleId); + + if(!articleId) { + throw new Error('게시글을 찾을 수 없습니다.') + } + + const articleView = await GetArticleHandler.handle(requester, { + articleId, + }); + + res.status(201).send(articleView); + }), +); + +// 게시글 수정 api +ArticleRouter.patch( + '/:articleId', + AuthN(), + asyncErrorHandler(async (req:Request< {articleId: TArticle;} >, res: Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const { articleId } = req.params; + const { title, content, image } = create(req.body, UpdateArticleRequestStruct); + + const articleView = await UpdateArticleHandler.handle(requester, { + articleId: Number(articleId), + title, + content, + image, + }); + + return res.status(201).send(articleView); + }), +); + +// 게시글 삭제 api +ArticleRouter.delete( + '/:articleId', + AuthN(), + asyncErrorHandler(async (req:Request< {articleId: number}>, res: Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const { articleId } = req.params; + + await DeleteArticleHandler.handle(requester, { + articleId: Number(articleId), + }); + + return res.status(204).send(); + }), +); + +// 게시글 목록 조회 api +ArticleRouter.get( + '/', + asyncErrorHandler(async (req:Request< {}, {}, {}, ArticleRouterQuery >, res: Response) => { + const requester = AuthTokenManager.getRequesterFromTokenOrDefault( + req.headers.authorization!, + ); + + const { cursor, limit, orderBy, keyword } = create(req.query, GetArticleListRequestStruct); + + const articleListView = await GetArticleListHandler.handle(requester, { + cursor, + limit, + orderBy, + keyword, + }); + + return res.send(articleListView); + }), +); + +// 게시글 댓글 등록 api +ArticleRouter.post( + '/:articleId/comments', + AuthN(), + asyncErrorHandler(async (req:Request< {articleId: number}, {}, {content: string}>, res:Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const { articleId } = req.params; + const { content } = create(req.body, CreateCommentRequestStruct); + + const articleCommentView = await CreateArticleCommentHandler.handle(requester, { + articleId: Number(articleId), + content, + }); + + return res.status(201).send(articleCommentView); + }), +); + +// 게시글 댓글 목록 조회 api +ArticleRouter.get( + '/:articleId/comments', + asyncErrorHandler(async (req: Request<{articleId: number}, {}, {}, ArticleRouterQuery>, res: Response) => { + const { articleId } = req.params; + const { cursor, limit } = create(req.query, GetCommentListRequestStruct); + + const articleCommentListView = await GetArticleCommentListHandler.handle({ + articleId: Number(articleId), + cursor, + take: limit ? Number(limit) : undefined, + }); + + return res.send(articleCommentListView); + }), +); + +// 게시글 좋아요 API +ArticleRouter.post( + '/:articleId/like', + AuthN(), + asyncErrorHandler(async (req: Request < {articleId: TArticle}, {}, {} >, res: Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const articleId = Number(req.params.articleId); + + const articleView = await CreateArticleLikeHandler.handle(requester, { + articleId, + }); + + return res.status(201).send(articleView); + }), +); + +// 게시글 좋아요 취소 API +ArticleRouter.delete( + '/:articleId/like', + AuthN(), + asyncErrorHandler(async (req: Request < {articleId: TArticle}, {}, {} >, res: Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const articleId = Number(req.params.articleId); + + const articleView = await DeleteArticleLikeHandler.handle(requester, { + articleId, + }); + + return res.status(201).send(articleView); + }), +); diff --git a/src/interface/AuthRouter.ts b/src/interface/AuthRouter.ts new file mode 100644 index 00000000..a31bb25e --- /dev/null +++ b/src/interface/AuthRouter.ts @@ -0,0 +1,112 @@ +import express, { Request, Response } from 'express'; +import { create } from 'superstruct'; + +import { asyncErrorHandler } from './utils/asyncErrorHandler'; + +import { SignUpRequestStruct } from './structs/auth/SignUpRequestStruct'; +import { SignInRequestStruct } from './structs/auth/SignInRequestStruct'; +import { RefreshTokenRequestStruct } from './structs/auth/RefreshTokenRequestStruct'; + +import { SignUpLocalUserHandler } from '../application/auth/SignUpLocalUserHandler'; +import { SignInLocalUserHandler } from '../application/auth/SignInLocalUserHandler'; +import { RefreshTokenHandler } from '../application/auth/RefreshTokenHandler'; +import { AuthByGoogleHandler } from '../application/auth/AuthByGoogleHandler'; +import { googleOAuthHelper } from '../infra/GoogleOAuthAdapter'; +import { User } from '@prisma/client'; + +export const AuthRouter = express.Router(); + +type AuthRouterRequest = User & { + passwordConfirmation: string; +} + +// 회원가입 api +AuthRouter.post( + '/signUp', + asyncErrorHandler(async (req : Request<{}, {}, AuthRouterRequest>, res: Response) => { + const { email, nickname, password, passwordConfirmation } = create( + req.body, + SignUpRequestStruct, + ); + + const userView = await SignUpLocalUserHandler.handle({ + email, + nickname, + password, + passwordConfirmation, + }); + + return res.status(201).send(userView); + }), +); + +// 로그인 api +AuthRouter.post( + '/signIn', + asyncErrorHandler(async (req : Request<{}, {}, Pick>, res: Response) => { + const { email, password } = create(req.body, SignInRequestStruct); + + const userView = await SignInLocalUserHandler.handle({ + email, + password, + }); + + return res.send(userView); + }), +); + +// 토큰 갱신 api +AuthRouter.post( + '/refresh-token', + asyncErrorHandler(async (req : Request<{},{},{refreshToken: string;}>, res: Response) => { + const { refreshToken } = create(req.body, RefreshTokenRequestStruct); + + const accessTokenView = await RefreshTokenHandler.handle({ + refreshToken, + }); + + return res.send(accessTokenView); + }), +); + +/** + * 구글 로그인 또는 회원가입을 시작하는 API + * + * 워크 플로: + * 웹 브라우저가 이 API로 GET 메서드와 함께 접속하면, + * 1. 구글 consent screen (구글 로그인 페이지) 주소로 리다이렉트 시켜줍니다. + * 2. 사용자가 구글에서 로그인을 마치면 아래의 `/google/callback`으로 리다이렉트되어 돌아옵니다. + */ +// AuthRouter.get('/google', asyncErrorHandler(async (req : Request, res: Response) => { +// const redirectURI = googleOAuthHelper.generateAuthURI(); +// return res.status(302).redirect(redirectURI); +// })); + +/** + * 구글 로그인 또는 회원가입 + * + * 워크 플로: + * 1. 구글 consent screen에서 로그인을 완료하면 구글에서는 code 값을 쿼리 스트링으로 붙여 이 API로 리다이렉트 시켜준다. + * 2. 사용자의 웹 브라우저는 code 값과 함께 이 API로 GET 요청을 보낸다. + * 3. 백엔드 서버에서는 받은 code를 사용해 구글에서 사용자 데이터를 가져온다. + * 4. 로그인 또는 회원가입 처리를 하고나서 Access Token 및 Refresh Token을 발급해서 쿼리 스트링으로 붙인 다음, + * 5. 클라이언트 페이지로 다시 리다이렉트 시켜준다. + * 6. Access Token과 Refresh Token을 받은 클라이언트 사이트에서는 이것을 사용해 로그인한다. + */ +// AuthRouter.get( +// '/google/callback', +// asyncErrorHandler(async (req : Request<{code: string | undefined}>, res: Response) => { +// const { code } = req.query; + +// const { accessToken, refreshToken } = await AuthByGoogleHandler.handle({ +// code, +// }); + +// const searchParams = new URLSearchParams({ +// at: accessToken, +// rt: refreshToken, +// }); + +// return res.status(302).redirect(`${process.env.CLIENT_REDIRECT_URI}/?${searchParams.toString()}`); +// }), +// ); diff --git a/src/interface/CommentRouter.ts b/src/interface/CommentRouter.ts new file mode 100644 index 00000000..340f162e --- /dev/null +++ b/src/interface/CommentRouter.ts @@ -0,0 +1,50 @@ +import express, { Request, Response } from 'express'; +import { create } from 'superstruct'; + +import { AuthTokenManager } from '../infra/AuthTokenManager'; + +import { asyncErrorHandler } from './utils/asyncErrorHandler'; +import { AuthN } from './utils/AuthN'; + +import { UpdateCommentRequestStruct } from './structs/comment/UpdateCommentRequestStruct'; + +import { UpdateCommentHandler } from '../application/comment/UpdateCommentHandler'; +import { DeleteCommentHandler } from '../application/comment/DeleteCommentHandler'; + +export const CommentRouter = express.Router(); + +// 댓글 수정 api +CommentRouter.patch( + '/:commentId', + AuthN(), + asyncErrorHandler(async (req: Request<{commentId: string;}, {}, {content: string;}>, res: Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const { commentId } = req.params; + const { content } = create(req.body, UpdateCommentRequestStruct); + + const commentView = await UpdateCommentHandler.handle(requester, { + commentId: Number(commentId), + content, + }); + + return res.send(commentView); + }), +); + +// 댓글 삭제 api +CommentRouter.delete( + '/:commentId', + AuthN(), + asyncErrorHandler(async (req: Request<{commentId: string;}>, res: Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const { commentId } = req.params; + + await DeleteCommentHandler.handle(requester, { + commentId: Number(commentId), + }); + + return res.status(204).send(); + }), +); diff --git a/src/interface/ImageRouter.ts b/src/interface/ImageRouter.ts new file mode 100644 index 00000000..232e14f3 --- /dev/null +++ b/src/interface/ImageRouter.ts @@ -0,0 +1,48 @@ +import express, { Request, Response } from 'express'; +import multer from 'multer'; +import path from 'path'; + +import { asyncErrorHandler } from './utils/asyncErrorHandler'; +import { AuthN } from './utils/AuthN'; + +export const ImageRouter = express.Router(); + +type MylterRequestType = Request& { + file: Express.Multer.File; +} + +const imageUpload = multer({ + storage: multer.diskStorage({ + destination: function (req, file, cb) { + cb(null, path.join(path.resolve(), 'public/images/')); + }, + filename: function (req, file, cb) { + cb(null, [Date.now(), file.originalname].join('-')); + }, + }), + + limits: { + fileSize: 5 * 1024 * 1024, + }, + + fileFilter: function (req, file, cb) { + if (['image/png', 'image/jpeg'].includes(file.mimetype) === false) { + return cb(new Error('Only png and jpeg are allowed')); + } + + cb(null, true); + }, +}); + +// 파일 업로드 API +ImageRouter.post( + '/upload', + AuthN(), + imageUpload.single('image'), + asyncErrorHandler(async (req: MylterRequestType, res: Response) => { + const filePath = path.join('static/images/', req.file.filename); + return res.send({ + url: `${process.env.BASE_URL}/${filePath}`, + }); + }), +); diff --git a/src/interface/ProductRouter.ts b/src/interface/ProductRouter.ts new file mode 100644 index 00000000..aa70f5bc --- /dev/null +++ b/src/interface/ProductRouter.ts @@ -0,0 +1,217 @@ +import express, { Request, Response } from 'express'; +import { create } from 'superstruct'; + +import { AuthTokenManager } from '../infra/AuthTokenManager'; + +import { asyncErrorHandler } from './utils/asyncErrorHandler'; +import { AuthN } from './utils/AuthN'; + +import { CreateProductRequestStruct } from './structs/product/CreateProductRequestStruct'; +import { UpdateProductRequestStruct } from './structs/product/UpdateProductRequestStruct'; +import { GetProductListRequestStruct } from './structs/product/GetProductListRequestStruct'; +import { CreateCommentRequestStruct } from './structs/comment/CreateCommentRequestStruct'; +import { GetCommentListRequestStruct } from './structs/comment/GetCommentListRequestStruct'; + +import { CreateProductHandler } from '../application/product/CreateProductHandler'; +import { GetProductHandler } from '../application/product/GetProductHandler'; +import { UpdateProductHandler } from '../application/product/UpdateProductHandler'; +import { DeleteProductHandler } from '../application/product/DeleteProductHandler'; +import { GetProductListHandler } from '../application/product/GetProductListHandler'; +import { CreateProductCommentHandler } from '../application/product/CreateProductCommentHandler'; +import { GetProductCommentListHandler } from '../application/product/GetProductCommentListHandler'; +import { CreateProductLikeHandler } from '../application/product/CreateProductLikeHandler'; +import { DeleteProductLikeHandler } from '../application/product/DeleteProductLikeHandler'; + +export const ProductRouter = express.Router(); + +type TProductRequestBody = { + name: string; + description: string; + price: string; + tags: string; + images: string; +} + +type TProductQuery = { + page: number; + pageSize: number; + orderBy: "recent" | "favorite"; + keyword: string | undefined; +} + +// 상품 등록 api +ProductRouter.post( + '/', + AuthN(), + asyncErrorHandler(async (req: Request<{}, {}, TProductRequestBody>, res: Response) : Promise => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const { name, description, price, tags, images } = create( + req.body, + CreateProductRequestStruct, + ); + + const productView = await CreateProductHandler.handle(requester, { + name, + description, + price, + tags, + images, + }); + + res.status(201).send(productView); + }), +); + +// 상품 조회 api +ProductRouter.get( + '/:productId', + asyncErrorHandler(async (req: Request, res: Response) => { + const requester = AuthTokenManager.getRequesterFromTokenOrDefault( + req.headers.authorization!, + ); + + const { productId } = req.params; + + const productView = await GetProductHandler.handle(requester, { + productId: Number(productId), + }); + + return res.send(productView); + }), +); + +// 상품 수정 api +ProductRouter.patch( + '/:productId', + AuthN(), + asyncErrorHandler(async (req: Request, res: Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const { productId } = req.params; + const { name, description, price, tags, images } = create( + req.body, + UpdateProductRequestStruct, + ); + + const productView = await UpdateProductHandler.handle(requester, { + productId: Number(productId), + name, + description, + price, + tags, + images, + }); + + return res.send(productView); + }), +); + +// 상품 삭제 api +ProductRouter.delete( + '/:productId', + AuthN(), + asyncErrorHandler(async (req: Request, res: Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const { productId } = req.params; + + await DeleteProductHandler.handle(requester, { + productId: Number(productId), + }); + + return res.status(204).send(); + }), +); + + +// 상품 목록 조회 api +ProductRouter.get( + '/', + asyncErrorHandler(async (req: Request<{}, {}, {}, TProductQuery>, res: Response) => { + const requester = AuthTokenManager.getRequesterFromTokenOrDefault( + req.headers.authorization!, + ); + + const { page, pageSize, orderBy, keyword } = create(req.query, GetProductListRequestStruct); + + const productListView = await GetProductListHandler.handle(requester, { + page, + pageSize, + orderBy, + keyword, + }); + + return res.send(productListView); + }), +); + +// 상품 댓글 등록 api +ProductRouter.post( + '/:productId/comments', + AuthN(), + asyncErrorHandler(async (req: Request<{productId: number}>, res: Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const { productId } = req.params; + const { content } = create(req.body, CreateCommentRequestStruct); + + const productCommentView = await CreateProductCommentHandler.handle(requester, { + productId: Number(productId), + content, + }); + + return res.status(201).send(productCommentView); + }), +); + +// 상품 댓글 목록 조회 api +ProductRouter.get( + '/:productId/comments', + asyncErrorHandler(async (req: Request, res: Response) => { + const { productId } = req.params; + const { cursor, limit } = create(req.query, GetCommentListRequestStruct); + + const productCommentListView = await GetProductCommentListHandler.handle({ + productId: Number(productId), + cursor, + limit, + }); + + return res.send(productCommentListView); + }), +); + +// 상품 좋아요 API +ProductRouter.post( + '/:productId/favorite', + AuthN(), + asyncErrorHandler(async (req: Request, res: Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const productId = Number(req.params.productId); + + const productView = await CreateProductLikeHandler.handle(requester, { + productId, + }); + + return res.status(201).send(productView); + }), +); + +// 상품 좋아요 취소 API +ProductRouter.delete( + '/:productId/favorite', + AuthN(), + asyncErrorHandler(async (req: Request, res: Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const productId = Number(req.params.productId); + + const productView = await DeleteProductLikeHandler.handle(requester, { + productId, + }); + + return res.status(201).send(productView); + }), +); diff --git a/src/interface/UserRouter.ts b/src/interface/UserRouter.ts new file mode 100644 index 00000000..203bdba1 --- /dev/null +++ b/src/interface/UserRouter.ts @@ -0,0 +1,113 @@ +import express, { Request, Response } from 'express'; +import { create } from 'superstruct'; + +import { AuthTokenManager } from '../infra/AuthTokenManager'; + +import { asyncErrorHandler } from './utils/asyncErrorHandler'; +import { AuthN } from './utils/AuthN'; + +import { UpdateProfileRequestStruct } from './structs/user/UpdateProfileRequestStruct'; +import { UpdatePasswordRequestStruct } from './structs/user/UpdatePasswordRequestStruct'; +import { GetMyProductListRequestStruct } from './structs/user/GetMyProductListRequestStruct'; +import { GetMyFavoritesProductListRequestStruct } from './structs/user/GetMyFavoritesProductListRequestStruct'; +import { GetUserProfileHandler } from '../application/user/GetUserProfileHandler'; +import { UpdateUserProfileHandler } from '../application/user/UpdateUserProfileHandler'; +import { UpdateUserPasswordHandler } from '../application/user/UpdateUserPasswordHandler'; +import { GetUserProductListHandler } from '../application/user/GetUserProductListHandler'; +import { GetUserFavoriteListHandler } from '../application/user/GetUserFavoriteListHandler'; +import { User } from '@prisma/client'; + +export const UserRouter = express.Router(); + +// 내 정보 조회하기 api +UserRouter.get( + '/me', + AuthN(), + asyncErrorHandler(async (req: Request, res: Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const userView = await GetUserProfileHandler.handle(requester); + + return res.send(userView); + }), +); + +// 내 정보 수정하기 api +UserRouter.patch( + '/me', + AuthN(), + asyncErrorHandler(async (req: Request<{}, {}, {image: string | null}>, res: Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const { image } = create(req.body, UpdateProfileRequestStruct); + + const userView = await UpdateUserProfileHandler.handle(requester, { + image, + }); + + return res.send(userView); + }), +); + +// 내 패스워드 수정하기 api +UserRouter.patch( + '/me/password', + AuthN(), + asyncErrorHandler(async (req: Request, res: Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const { password, passwordConfirmation, currentPassword } = create( + req.body, + UpdatePasswordRequestStruct, + ); + + const userView = await UpdateUserPasswordHandler.handle(requester, { + password, + passwordConfirmation, + currentPassword, + }); + + return res.send(userView); + }), +); + +// 내가 등록한 상품 조회하기 api +UserRouter.get( + '/me/products', + AuthN(), + asyncErrorHandler(async (req: Request, res: Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const { page, pageSize, keyword } = create(req.query, GetMyProductListRequestStruct); + + const productListView = await GetUserProductListHandler.handle(requester, { + page, + pageSize, + keyword, + }); + + return res.send(productListView); + }), +); + +// 내가 좋아요한 상품 조회하기 api +UserRouter.get( + '/me/favorites', + AuthN(), + asyncErrorHandler(async (req: Request, res: Response) => { + const requester = AuthTokenManager.getRequesterFromToken(req.headers.authorization); + + const { page, pageSize, keyword } = create( + req.query, + GetMyFavoritesProductListRequestStruct, + ); + + const favoriteListView = await GetUserFavoriteListHandler.handle(requester, { + page, + pageSize, + keyword, + }); + + return res.send(favoriteListView); + }), +); diff --git a/src/interface/readme.md b/src/interface/readme.md new file mode 100644 index 00000000..7c7a7a96 --- /dev/null +++ b/src/interface/readme.md @@ -0,0 +1,52 @@ + +# Router + +라우팅이란 클라이언트가 서버의 특정 엔드포인트에 접근했을 때, 서버가 응답하는 방식을 의미합니다. + +여기서는 각각의 관심사 별로 라우터를 분리하여 구현하였으며, +이러한 방식은 코드의 유지보수를 쉽게 할 수 있도록 도와줍니다. + +**관심사 별로 라우터를 분리한 예시** + +- ArticleRouter +- CommentRouter +- ImageRouter +- ProductRouter + +**어떤 측면에서 유지보수가 좋아지나요?** + +- 각 라우터 하나하나의 라인수가 너무 커지는 것을 방지 +- 한 라우터를 수정 시, 다른 라우터에 영향이 가는 것을 방지 +- 여러 개발자가 동시에 협엽 시, 버전 관리 충돌을 최소화 할 수 있음 +- 각 라우터별로 최적화된 미들웨어를 사용하여, 접근제어나 캐싱전략을 다르게 가져갈 수 있음 + +## 요청 플로우 + +해당 어플리케이션에서는 클라이언트의 요청이 들어왔을 때, 다음과 같이 처리합니다. + +```mermaid +flowchart TD + A[요청이 들어옴] --> B{해당 요청을 처리할 수 있는 라우팅 함수가 있는가?} + B --> |없다면| X[404 Not Found] + B --> |있다면, 해당 라우팅 함수로 이동| C{superstruct 를 통하여 \n 필요 시 페이로드를 전처리하고, \n 우리의 의도에 맞게 보냈는지 판단} + C -->|올바르지 않은 페이로드| D[404 Bad Request] + C --> E(서비스 로직 처리 시도) + E -->|성공| F[2xx 응답 반환] + E -->|관련 자원을 찾을 수 없음| G[404 Not Found] + E -->|그 외의 에러| H[500 Internal Server Error] +``` + +### 유효성을 검사하기 전에, 페이로드 전처리는 왜 필요한가요? + +전처리가 필요한 유효성 검사 케이스는 생각보다 흔합니다. + +예를 들어, + +- 상품 생성 요청 시, ProductName 에는 `앞뒤로 공백이 없어야 하며` 제목이 2글자 이상 되게끔 해주세요. +- 상품 목록 조회 시, cursor 값이 문자열로 오고 있어요, `미리미리 숫자로 바꿔서 사용` 하고 싶어요. +- 상품 목록 조회 시, cursor 값이 명시적으로 주어지지 않았다면 `기본값으로 0을 사용` 하게끔 해주세요. + +이러한 전처리는 라우팅 함수 내에서 해도 결과적으로는 문제가 없습니다. + +하지만 라우팅 함수가 처리로직에만 집중할 수 있도록, +처리로직 이외의 동작은 다른 코드에 위임하는 것이 좋습니다. diff --git a/src/interface/structs/article/CreateArticleRequestStruct.ts b/src/interface/structs/article/CreateArticleRequestStruct.ts new file mode 100644 index 00000000..63dc90ba --- /dev/null +++ b/src/interface/structs/article/CreateArticleRequestStruct.ts @@ -0,0 +1,20 @@ +import { coerce, nullable, object, nonempty, string, defaulted } from 'superstruct'; + +export const CreateArticleRequestStruct = object({ + /** + * [데이터 전처리 - 데이터 변환] + * + * coerce 메서드는 데이터 변환을 같이 수행합니다. + * + * 파라미터로 (struct, condition, transformer) 를 받으며, 아래와 같이 동작합니다. + * 1. condition 에 일치한 경우 transformer 를 실행하여 값을 변환합니다. + * 2. 변환된 값이 struct 에 맞는지 검사합니다. + * + * 아래 코드는 title 의 앞뒤 공백을 제거합니다. + * + * @see https://docs.superstructjs.org/api-reference/coercions#custom-coercions + */ + title: coerce(nonempty(string()), string(), (value) => value.trim()), // 또는 trimmed() 를 사용하여 구현할 수 있습니다. + content: nonempty(string()), + image: nullable(string()), +}); diff --git a/src/interface/structs/article/GetArticleListRequestStruct.ts b/src/interface/structs/article/GetArticleListRequestStruct.ts new file mode 100644 index 00000000..8fa6876c --- /dev/null +++ b/src/interface/structs/article/GetArticleListRequestStruct.ts @@ -0,0 +1,40 @@ +import { + coerce, + optional, + object, + integer, + string, + min, + max, + enums, + nonempty, + defaulted, +} from 'superstruct'; + +export const GetArticleListRequestStruct = object({ + /** + * [데이터 전처리 - 기본값 설정] + * + * defaulted 메서드는 기본값을 설정하기 위해 사용됩니다. + * + * 파라미터로는 (struct, defaultValue) 를 받으며, 아래와 같이 동작합니다. + * 1. 값이 undefined 인 경우, defaultValue 를 반환합니다. + * 2. 값이 undefined 가 아닌 경우, struct 에 맞는지 검사합니다. 추가적인 변환이 수행될 수 있습니다. + * + * 아래 코드는 cursor 가 undefined 인 경우, 0 을 반환합니다. + * + * @see https://docs.superstructjs.org/api-reference/coercions#defaulted + */ + cursor: defaulted( + coerce(min(integer(), 0), string(), (value) => Number.parseInt(value, 10)), + 0 + ), + limit: defaulted( + coerce(max(min(integer(), 1), 10), string(), (value) => + Number.parseInt(value, 10) + ), + 10 + ), + orderBy: defaulted(enums(['recent', 'favorite']), 'recent'), + keyword: optional(nonempty(string())), +}); diff --git a/src/interface/structs/article/UpdateArticleRequestStruct.ts b/src/interface/structs/article/UpdateArticleRequestStruct.ts new file mode 100644 index 00000000..a0683c4b --- /dev/null +++ b/src/interface/structs/article/UpdateArticleRequestStruct.ts @@ -0,0 +1,5 @@ +import { partial } from 'superstruct'; + +import { CreateArticleRequestStruct } from './CreateArticleRequestStruct'; + +export const UpdateArticleRequestStruct = partial(CreateArticleRequestStruct); diff --git a/src/interface/structs/auth/RefreshTokenRequestStruct.ts b/src/interface/structs/auth/RefreshTokenRequestStruct.ts new file mode 100644 index 00000000..07884245 --- /dev/null +++ b/src/interface/structs/auth/RefreshTokenRequestStruct.ts @@ -0,0 +1,5 @@ +import { object, nonempty, string } from 'superstruct'; + +export const RefreshTokenRequestStruct = object({ + refreshToken: nonempty(string()), +}); diff --git a/src/interface/structs/auth/SignInRequestStruct.ts b/src/interface/structs/auth/SignInRequestStruct.ts new file mode 100644 index 00000000..8a5553ba --- /dev/null +++ b/src/interface/structs/auth/SignInRequestStruct.ts @@ -0,0 +1,7 @@ +import { object, nonempty, string, define } from 'superstruct'; +import isEmail from 'is-email'; + +export const SignInRequestStruct = object({ + email: define('Email', (value: unknown) => typeof value === 'string' && isEmail(value)), + password: nonempty(string()), +}); diff --git a/src/interface/structs/auth/SignUpRequestStruct.ts b/src/interface/structs/auth/SignUpRequestStruct.ts new file mode 100644 index 00000000..2b19e2c0 --- /dev/null +++ b/src/interface/structs/auth/SignUpRequestStruct.ts @@ -0,0 +1,9 @@ +import { coerce, object, nonempty, string, define, boolean } from 'superstruct'; +import isEmail from 'is-email'; + +export const SignUpRequestStruct = object({ + email: define('Email', (value: unknown) => typeof value === 'string' && isEmail(value)), + nickname: coerce(nonempty(string()), string(), (value) => value.trim()), + password: nonempty(string()), + passwordConfirmation: nonempty(string()), +}); diff --git a/src/interface/structs/comment/CreateCommentRequestStruct.ts b/src/interface/structs/comment/CreateCommentRequestStruct.ts new file mode 100644 index 00000000..2a93d668 --- /dev/null +++ b/src/interface/structs/comment/CreateCommentRequestStruct.ts @@ -0,0 +1,5 @@ +import { nonempty, object, string } from 'superstruct'; + +export const CreateCommentRequestStruct = object({ + content: nonempty(string()), +}); diff --git a/src/interface/structs/comment/GetCommentListRequestStruct.ts b/src/interface/structs/comment/GetCommentListRequestStruct.ts new file mode 100644 index 00000000..53e3d288 --- /dev/null +++ b/src/interface/structs/comment/GetCommentListRequestStruct.ts @@ -0,0 +1,12 @@ +import { coerce, defaulted, object, string, min, max, integer } from 'superstruct'; + +export const GetCommentListRequestStruct = object({ + cursor: defaulted( + coerce(min(integer(), 0), string(), (value) => Number.parseInt(value, 10)), + 0, + ), + limit: defaulted( + coerce(max(min(integer(), 1), 10), string(), (value) => Number.parseInt(value, 10)), + 10, + ), +}); diff --git a/src/interface/structs/comment/UpdateCommentRequestStruct.ts b/src/interface/structs/comment/UpdateCommentRequestStruct.ts new file mode 100644 index 00000000..921526ab --- /dev/null +++ b/src/interface/structs/comment/UpdateCommentRequestStruct.ts @@ -0,0 +1,5 @@ +import { partial } from 'superstruct'; + +import { CreateCommentRequestStruct } from './CreateCommentRequestStruct'; + +export const UpdateCommentRequestStruct = partial(CreateCommentRequestStruct); diff --git a/src/interface/structs/product/CreateProductRequestStruct.ts b/src/interface/structs/product/CreateProductRequestStruct.ts new file mode 100644 index 00000000..1d1bac63 --- /dev/null +++ b/src/interface/structs/product/CreateProductRequestStruct.ts @@ -0,0 +1,9 @@ +import { coerce, object, nonempty, string, min, integer, array } from 'superstruct'; + +export const CreateProductRequestStruct = object({ + name: coerce(nonempty(string()), string(), (value) => value.trim()), + description: nonempty(string()), + price: min(integer(), 0), + tags: array(nonempty(string())), + images: array(nonempty(string())), +}); diff --git a/src/interface/structs/product/GetProductListRequestStruct.ts b/src/interface/structs/product/GetProductListRequestStruct.ts new file mode 100644 index 00000000..0984662e --- /dev/null +++ b/src/interface/structs/product/GetProductListRequestStruct.ts @@ -0,0 +1,27 @@ +import { + coerce, + optional, + object, + integer, + string, + min, + max, + enums, + nonempty, + defaulted, +} from 'superstruct'; + +export const GetProductListRequestStruct = object({ + page: defaulted( + coerce(min(integer(), 1), string(), (value) => Number.parseInt(value, 10)), + 1 + ), + pageSize: defaulted( + coerce(max(min(integer(), 1), 12), string(), (value) => + Number.parseInt(value, 10) + ), + 10 + ), + orderBy: defaulted(enums(['recent', 'favorite']), 'recent'), + keyword: optional(nonempty(string())), +}); diff --git a/src/interface/structs/product/UpdateProductRequestStruct.ts b/src/interface/structs/product/UpdateProductRequestStruct.ts new file mode 100644 index 00000000..49ae3f97 --- /dev/null +++ b/src/interface/structs/product/UpdateProductRequestStruct.ts @@ -0,0 +1,5 @@ +import { partial } from 'superstruct'; + +import { CreateProductRequestStruct } from './CreateProductRequestStruct'; + +export const UpdateProductRequestStruct = partial(CreateProductRequestStruct); diff --git a/src/interface/structs/user/GetMyFavoritesProductListRequestStruct.ts b/src/interface/structs/user/GetMyFavoritesProductListRequestStruct.ts new file mode 100644 index 00000000..a879e7ca --- /dev/null +++ b/src/interface/structs/user/GetMyFavoritesProductListRequestStruct.ts @@ -0,0 +1,23 @@ +import { + coerce, + optional, + object, + integer, + string, + min, + max, + nonempty, + defaulted, +} from 'superstruct'; + +export const GetMyFavoritesProductListRequestStruct = object({ + page: defaulted( + coerce(min(integer(), 0), string(), (value) => Number.parseInt(value, 10)), + 1, + ), + pageSize: defaulted( + coerce(max(min(integer(), 1), 10), string(), (value) => Number.parseInt(value, 10)), + 10, + ), + keyword: optional(nonempty(string())), +}); diff --git a/src/interface/structs/user/GetMyProductListRequestStruct.ts b/src/interface/structs/user/GetMyProductListRequestStruct.ts new file mode 100644 index 00000000..46d24977 --- /dev/null +++ b/src/interface/structs/user/GetMyProductListRequestStruct.ts @@ -0,0 +1,24 @@ +import { + coerce, + optional, + object, + integer, + string, + min, + max, + enums, + nonempty, + defaulted, +} from 'superstruct'; + +export const GetMyProductListRequestStruct = object({ + page: defaulted( + coerce(min(integer(), 0), string(), (value) => Number.parseInt(value, 10)), + 1, + ), + pageSize: defaulted( + coerce(max(min(integer(), 1), 10), string(), (value) => Number.parseInt(value, 10)), + 10, + ), + keyword: optional(nonempty(string())), +}); diff --git a/src/interface/structs/user/UpdatePasswordRequestStruct.ts b/src/interface/structs/user/UpdatePasswordRequestStruct.ts new file mode 100644 index 00000000..6b4cd276 --- /dev/null +++ b/src/interface/structs/user/UpdatePasswordRequestStruct.ts @@ -0,0 +1,7 @@ +import { nonempty, object, string } from 'superstruct'; + +export const UpdatePasswordRequestStruct = object({ + password: nonempty(string()), + passwordConfirmation: nonempty(string()), + currentPassword: nonempty(string()), +}); diff --git a/src/interface/structs/user/UpdateProfileRequestStruct.ts b/src/interface/structs/user/UpdateProfileRequestStruct.ts new file mode 100644 index 00000000..e9ad4fa5 --- /dev/null +++ b/src/interface/structs/user/UpdateProfileRequestStruct.ts @@ -0,0 +1,5 @@ +import { nullable, object, string } from 'superstruct'; + +export const UpdateProfileRequestStruct = object({ + image: nullable(string()), +}); diff --git a/src/interface/utils/AuthN.ts b/src/interface/utils/AuthN.ts new file mode 100644 index 00000000..55ee3889 --- /dev/null +++ b/src/interface/utils/AuthN.ts @@ -0,0 +1,31 @@ +import { NextFunction, Request, Response } from 'express'; +import { AuthTokenManager } from '../../infra/AuthTokenManager'; + +/** + * 인증 미들웨어 + * + * HTTP 메시지에서 authorization 헤더로 전달된 JWT 토큰을 검증합니다. + * 아래 케이스의 경우 401 Unauthorized 응답을 반환합니다. + * + * - JWT 토큰이 전달되지 않은 경우 + * - JWT 토큰이 유효하지 않은 경우 (ex 시크릿 키가 일치하지 않는 경우) + * - JWT 토큰이 만료된 경우 + */ +export function AuthN() { + return async function (req:Request, res:Response, next:NextFunction): Promise { + const authHeader = req.headers.authorization; + const jwtToken = req?.headers?.authorization?.split(' ')[1]; // "bearer JWT_TOKEN" 형태로 전달받음 + + + if (!jwtToken || AuthTokenManager.isValidAccessToken(jwtToken) === false) { + res.status(401).send({ + name: 'Unauthorized', + message: 'Invalid JWT token', + }); + return; // 반드시 return으로 함수 종료! + } + + // 토큰이 유효하면 다음 미들웨어로 진행 + next(); + }; +} diff --git a/src/interface/utils/asyncErrorHandler.ts b/src/interface/utils/asyncErrorHandler.ts new file mode 100644 index 00000000..caf67b67 --- /dev/null +++ b/src/interface/utils/asyncErrorHandler.ts @@ -0,0 +1,50 @@ +import superstruct from 'superstruct'; + +import { HttpException } from '../../exceptions/HttpException.js'; +import { BadRequestException } from '../../exceptions/BadRequestException.js'; +import { InternalServerErrorException } from '../../exceptions/InternalServerErrorException.js'; +import { Request, Response } from 'express'; + +export function asyncErrorHandler(handler: any) { + return async function (req: Request, res: Response) { + try { + await handler(req, res); + } catch (e) { + // 에러처리 로직을 일관화하기 위해, HttpException 으로 변환합니다. + const httpException = mapToHttpException(e); + handleHttpException(httpException, res); + } + }; +} + +function handleHttpException(httpError: any, res: Response) { + res.status(httpError.status).send({ + name: httpError.name, + message: httpError.message, + }); +} + +/** + * [에러로직 일관화를 위한, Exception 변환 메서드] + * + * 해당 메서드는 항상 HttpException 을 반환합니다. + * + * 동작: + * 1. HttpException 이라면 그대로 반환합니다. + * 2. Known Error 라면, 해당 에러에 맞는 HttpException 으로 변환합니다. + * 3. Unknown Error 라면, InternalServerErrorException 으로 변환합니다. + */ +function mapToHttpException(e:any) { + if (e instanceof HttpException) { + return e; + } + + // Known Error + if (e instanceof superstruct.StructError) { + return new BadRequestException('Validation Failed', e.message); + } + + // 마지막까지 처리되지 않았다면, Unknown Error 입니다. + // InternalServerErrorException 으로 변환합니다. + return new InternalServerErrorException('Internal Server Error', e.message); +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 00000000..f5531524 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,59 @@ +import path from 'path'; +import dotenv from 'dotenv'; +dotenv.config({ path: path.join(path.resolve(), '.env') }); + +import express, { NextFunction, Request, Response } from 'express'; +import cors from 'cors'; +import morgan from 'morgan'; +import swaggerUi from 'swagger-ui-express'; +import yaml from 'yaml'; +import fs from 'fs'; + +import { AuthRouter } from './interface/AuthRouter'; +import { ArticleRouter } from './interface/ArticleRouter'; +import { ProductRouter } from './interface/ProductRouter'; +import { CommentRouter } from './interface/CommentRouter'; +import { ImageRouter } from './interface/ImageRouter'; +import { UserRouter } from './interface/UserRouter'; + +const app = express(); +app.use(cors()); +app.use(express.json()); +app.use(morgan('dev')); + +app.use( + '/api-docs', + swaggerUi.serve, + swaggerUi.setup( + yaml.parse(fs.readFileSync(path.join(path.resolve(), 'openapi.yaml'), 'utf-8')), + ), +); + +/** + * 현재 디렉터리의 public 폴더를 외부 브라우저에서 접근할 수 있도록 설정합니다. + * static 이라는 이름으로 접근할 수 있습니다. + * + * @example http://localhost:3000/static/images/sample-image.jpg + */ +app.use('/static', express.static(path.join(path.resolve(), 'public/'))); + +app.use('/auth', AuthRouter); +app.use('/articles', ArticleRouter); +app.use('/products', ProductRouter); +app.use('/comments', CommentRouter); +app.use('/images', ImageRouter); +app.use('/users', UserRouter); +app.use(( + err: any, + req: Request, + res:Response, + next:NextFunction + ) => { + console.error(err.stack); + res.status(500).send({ + message: '예기치 못한 오류가 발생했습니다.', + }); +}); + +const port = process.env.HTTP_PORT ?? 4000; +app.listen(port, () => console.log(`Server started on port: ${port}`)); diff --git a/src/readme.md b/src/readme.md new file mode 100644 index 00000000..51001d08 --- /dev/null +++ b/src/readme.md @@ -0,0 +1,73 @@ + +# Software Architecture + +해당 예제에서는 DDD(`Domain Driven Development`) 의 개념을 일부 차용하여 작성되었습니다. + +DDD 에서는 소프트웨어의 레이어를 다음과 같이 구분합니다. + +- Infra Layer (`src/infra`) +- Interface Layer (`src/interface`) +- Application Layer (`src/application`) +- Domain Layer (`src/domain`) + +## Domain Layer + +DB에 영속화되며 소프트웨어에서 주로 다루고 있는 관심사를 도메인이라고 합니다. + +판다 마켓에서는 다음과 같이 5개의 도메인을 다루고 있습니다. + +- Article +- Comment +- Like +- Product +- User + +각 도매인에서는 고유규칙을 가지고 있을 수 있으며, 이것을 `도메인 규칙` 이라고 합니다. + +아직 판다마켓에서는 도메인 규칙이 없지만, 도메인 규칙으로 다음 예시들을 들 수 있습니다. + +- 구글로 가입한 사용자는 패스워드로 로그인할 수 없다. +- 아카이브 상태의 게시글은 제목을 수정할 수 없다. +- ... + + +## Application Layer + +도메인 객체과 그 외의 의존성을 조합하여 기능을 구현하는 레이어입니다. + +예를 들어 `구글 로그인` 을 구현하기 위해서는 아래의 3개 준비물이 필요합니다. + +- User (도메인 객체) +- GoogleOAuthAdapter (의존성) +- Prisma Client (의존성) + +마찬가지로 `게시글에 좋아요` 를 구현하기 위해서는 아래의 3개 준비물이 필요합니다. + +- Article (도메인 객체) +- Like (도메인 객체) +- Prisma Client (의존성) + + +## Interface Layer + +사용자와의 상호작용을 주요 관심사로 다루는 레이어입니다. + +아래 항목들을 관리합니다. + +- API 인터페이스 정의 + - 요청 페이로드 형식 + - 요청 페이로드에 대한 유효성 검사 + - 응답 형식 +- 인증 +- 추가적인 인가 + + +## Infra Layer + +외부 의존성을 주요 관심사로 다루는 레이어입니다. + +- 사용하고 있는 DB 및 클라이언트 +- AuthToken 발급 방식 +- 유저 패스워드 암호화 방식 +- 구글 OAuth API 인터페이스 + diff --git a/src/types/article.ts b/src/types/article.ts new file mode 100644 index 00000000..55cb88fc --- /dev/null +++ b/src/types/article.ts @@ -0,0 +1,17 @@ +import { Prisma } from "@prisma/client"; + +export type TArticleUser = { + userId: number; +} + +export type TArticle = { + articleId: number; + title: string; + content: string; + image: string | null; + cursor?: number | undefined; + limit?: number | undefined; + take?: number | undefined; + orderBy? : 'favorite' | 'recent'; + keyword? : string | undefined; +} \ No newline at end of file diff --git a/src/types/error.ts b/src/types/error.ts new file mode 100644 index 00000000..a11410b8 --- /dev/null +++ b/src/types/error.ts @@ -0,0 +1,48 @@ +// src/types/errors.ts +export class AppError extends Error { + code?: number; // 선택적 속성으로 변경 + data?: any; // 에러핸들러에서 사용하는 data 속성도 추가 + + constructor(message: string, code?: number, data?: any) { + super(message); + this.code = code; + this.data = data; + this.name = "AppError"; + } +} + +// 자주 사용하는 에러들을 위한 편의 클래스들 +export class ValidationError extends AppError { + constructor(message: string, data?: any) { + super(message, 422, data); // 422는 기본값 + this.name = "ValidationError"; + } +} + +export class AuthenticationError extends AppError { + constructor(message: string, data?: any) { + super(message, 401, data); // 401은 기본값 + this.name = "AuthenticationError"; + } +} + +export class ServerError extends AppError { + constructor(message: string, data?: any) { + super(message, 500, data); // 500은 기본값 + this.name = "ServerError"; + } +} + +export class NotFoundError extends AppError { + constructor(message: string, data?: any) { + super(message, 404, data); // 404은 기본값 + this.name = "NotFoundError"; + } +} + +export class ForbiddenError extends AppError { + constructor(message: string, data?: any) { + super(message, 403, data); // 403은 기본값 + this.name = "ForbiddenError"; + } +} \ No newline at end of file diff --git a/src/types/express.d.ts b/src/types/express.d.ts new file mode 100644 index 00000000..8474e528 --- /dev/null +++ b/src/types/express.d.ts @@ -0,0 +1,9 @@ +import { Express } from "express"; + +declare global { + namespace Express{ + interface Request { + // 속성 추가 영역 + } + } +} \ No newline at end of file diff --git a/src/types/product.ts b/src/types/product.ts new file mode 100644 index 00000000..a798543a --- /dev/null +++ b/src/types/product.ts @@ -0,0 +1,15 @@ +import { Prisma } from "@prisma/client"; + +export type TProductUser = { + userId: number; +} + +export type TProduct = { + productId: number; + content: string; + name: string; + description: string; + price: number; + tags?: string[] | undefined; + images : string[] | Prisma.ProductCreateimagesInput | undefined; +} \ No newline at end of file diff --git a/swagger/components.js b/swagger/components.js new file mode 100644 index 00000000..e69de29b diff --git a/swagger/index.js b/swagger/index.js new file mode 100644 index 00000000..e69de29b diff --git a/swagger/info.js b/swagger/info.js new file mode 100644 index 00000000..e69de29b diff --git a/swagger/paths.js b/swagger/paths.js new file mode 100644 index 00000000..e69de29b diff --git a/swagger/swagger.js b/swagger/swagger.js new file mode 100644 index 00000000..2342dbc3 --- /dev/null +++ b/swagger/swagger.js @@ -0,0 +1,21 @@ +const swaggerJSDoc = require("swagger-jsdoc"); +const swaggerUi = require("swagger-ui-express"); + +const option = { + definition: { + openai: "3.0.0", + info: { + title: "나의 API", + versiton: "1.0.0", + }, + }, + apis: ["./routes/*.js"], +}; + +const specs = swaggerJSDoc(options); + +module.exports = { swaggerUi, specs }; + +router.get("/user", getUsers); + + diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..67c9fd54 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,117 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "libReplacement": true, /* Enable lib replacement. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "commonjs", /* Specify what module code is generated. */ + "rootDir": "./src", /* Specify the root folder within your source files. */ + // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + "baseUrl": ".", /* Specify the base directory to resolve non-relative module names. */ + "paths": { + "@/*" : ["src/*"] + }, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "./dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + }, + "include": ["src/**/*"], // 컴파일 대상 폴더 + "exclude": ["node_modules"] // 컴파일 제외 폴더 +} From 590a50662fc54dfcb1b7fd4d63c8d8dcfe36b6fd Mon Sep 17 00:00:00 2001 From: Sue Date: Tue, 17 Jun 2025 11:35:59 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=9E=91=EC=97=85=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 --- .../article/CreateArticleCommentHandler.ts | 4 ++++ src/application/article/CreateArticleHandler.ts | 4 ++++ .../article/CreateArticleLikeHandler.ts | 5 +++++ .../article/DeleteArticleLikeHandler.ts | 5 +++++ .../article/GetArticleCommentListHandler.ts | 1 + src/application/article/GetArticleHandler.ts | 3 +++ src/application/article/GetArticleListHandler.ts | 8 +++++++- src/application/article/UpdateArticleHandler.ts | 5 ++++- src/application/comment/UpdateCommentHandler.ts | 4 ++++ .../product/CreateProductCommentHandler.ts | 4 ++++ src/application/user/UpdateUserProfileHandler.ts | 3 ++- .../{ExceptionMessage.js => ExceptionMessage.ts} | 0 src/domain/{Article.js => Article.ts} | 12 +++++++++--- src/domain/{Comment.js => Comment.ts} | 4 +++- src/domain/{Like.js => Like.ts} | 4 +++- src/domain/{Product.js => Product.ts} | 6 ++++-- src/domain/{User.js => User.ts} | 10 ++++++---- ...dRequestException.js => BadRequestException.ts} | 2 +- ...ForbiddenException.js => ForbiddenException.ts} | 2 +- .../{HttpException.js => HttpException.ts} | 10 +++++++++- ...xception.js => InternalServerErrorException.ts} | 2 +- .../{NotFoundException.js => NotFoundException.ts} | 2 +- ...xception.js => UnprocessableEntityException.ts} | 2 +- src/types/article.ts | 13 ++++++++++++- src/types/comment.ts | 9 +++++++++ src/types/like.ts | 7 +++++++ src/types/product.ts | 14 ++++++++++++++ src/types/user.ts | 9 +++++++++ 28 files changed, 133 insertions(+), 21 deletions(-) rename src/constant/{ExceptionMessage.js => ExceptionMessage.ts} (100%) rename src/domain/{Article.js => Article.ts} (80%) rename src/domain/{Comment.js => Comment.ts} (91%) rename src/domain/{Like.js => Like.ts} (89%) rename src/domain/{Product.js => Product.ts} (93%) rename src/domain/{User.js => User.ts} (83%) rename src/exceptions/{BadRequestException.js => BadRequestException.ts} (81%) rename src/exceptions/{ForbiddenException.js => ForbiddenException.ts} (81%) rename src/exceptions/{HttpException.js => HttpException.ts} (77%) rename src/exceptions/{InternalServerErrorException.js => InternalServerErrorException.ts} (81%) rename src/exceptions/{NotFoundException.js => NotFoundException.ts} (80%) rename src/exceptions/{UnprocessableEntityException.js => UnprocessableEntityException.ts} (81%) create mode 100644 src/types/comment.ts create mode 100644 src/types/like.ts create mode 100644 src/types/user.ts diff --git a/src/application/article/CreateArticleCommentHandler.ts b/src/application/article/CreateArticleCommentHandler.ts index a3c15b39..eae425e1 100644 --- a/src/application/article/CreateArticleCommentHandler.ts +++ b/src/application/article/CreateArticleCommentHandler.ts @@ -48,6 +48,10 @@ export class CreateArticleCommentHandler { }, }); + if(!writerEntity) { + throw new Error('User Not Found') + } + const writer = new User(writerEntity); return { diff --git a/src/application/article/CreateArticleHandler.ts b/src/application/article/CreateArticleHandler.ts index b481343a..5e867c63 100644 --- a/src/application/article/CreateArticleHandler.ts +++ b/src/application/article/CreateArticleHandler.ts @@ -35,6 +35,10 @@ export class CreateArticleHandler { }, }); + if(!writerEntity) { + throw new Error('User Not Found') + } + const writer = new User(writerEntity); return { diff --git a/src/application/article/CreateArticleLikeHandler.ts b/src/application/article/CreateArticleLikeHandler.ts index 20121543..d48c38e9 100644 --- a/src/application/article/CreateArticleLikeHandler.ts +++ b/src/application/article/CreateArticleLikeHandler.ts @@ -52,6 +52,11 @@ export class CreateArticleLikeHandler { id: articleEntity.writerId, }, }); + + if(!writerEntity) { + throw new Error('User Not Found') + } + const writer = new User(writerEntity); return { diff --git a/src/application/article/DeleteArticleLikeHandler.ts b/src/application/article/DeleteArticleLikeHandler.ts index 49d6dc78..980bc464 100644 --- a/src/application/article/DeleteArticleLikeHandler.ts +++ b/src/application/article/DeleteArticleLikeHandler.ts @@ -54,6 +54,11 @@ export class DeleteArticleLikeHandler { id: articleEntity.writerId, }, }); + + if(!writerEntity) { + throw new Error('User Not Found') + } + const writer = new User(writerEntity); return { diff --git a/src/application/article/GetArticleCommentListHandler.ts b/src/application/article/GetArticleCommentListHandler.ts index 6cc8e257..47231633 100644 --- a/src/application/article/GetArticleCommentListHandler.ts +++ b/src/application/article/GetArticleCommentListHandler.ts @@ -38,6 +38,7 @@ export class GetArticleCommentListHandler { }); }); + const comments = commentEntities.map((commentEntity) => new Comment(commentEntity)); const writerEntities = await prismaClient.user.findMany({ diff --git a/src/application/article/GetArticleHandler.ts b/src/application/article/GetArticleHandler.ts index 73d0e69a..dd8dabca 100644 --- a/src/application/article/GetArticleHandler.ts +++ b/src/application/article/GetArticleHandler.ts @@ -40,6 +40,9 @@ export class GetArticleHandler { }, }); + if(!writerEntity) { + throw new NotFoundException('Not Found', ExceptionMessage.ARTICLE_NOT_FOUND) + } const writer = new User(writerEntity); return { diff --git a/src/application/article/GetArticleListHandler.ts b/src/application/article/GetArticleListHandler.ts index 08461bc5..a1bd20ec 100644 --- a/src/application/article/GetArticleListHandler.ts +++ b/src/application/article/GetArticleListHandler.ts @@ -60,7 +60,13 @@ export class GetArticleListHandler { }, }); - const likes = likeEntities.map((likeEntity) => new Like(likeEntity)); + const likes = likeEntities.map((likeEntity) => new Like({ + id: likeEntity.id, + userId: likeEntity.userId, + productId: likeEntity.productId ?? 0, + articleId: likeEntity.articleId ?? 0, + createdAt: likeEntity.createdAt ?? new Date(), + })); const hasNext = articles.length === Number(limit) + 1; diff --git a/src/application/article/UpdateArticleHandler.ts b/src/application/article/UpdateArticleHandler.ts index 512c37bf..00ea85ba 100644 --- a/src/application/article/UpdateArticleHandler.ts +++ b/src/application/article/UpdateArticleHandler.ts @@ -6,7 +6,7 @@ import { ExceptionMessage } from '../../constant/ExceptionMessage'; import { Article } from '../../domain/Article'; import { User } from '../../domain/User'; -import { TArticle, TArticleUser } from '@/types/article'; +import { TArticleUser } from '@/types/article'; type TUpdateArticle = { articleId: number; @@ -60,6 +60,9 @@ export class UpdateArticleHandler { }, }); + if(!writerEntity) { + throw new Error('User Not Found') + } const writer = new User(writerEntity); return { diff --git a/src/application/comment/UpdateCommentHandler.ts b/src/application/comment/UpdateCommentHandler.ts index 84c7d16f..3f1751f1 100644 --- a/src/application/comment/UpdateCommentHandler.ts +++ b/src/application/comment/UpdateCommentHandler.ts @@ -51,6 +51,10 @@ export class UpdateCommentHandler { }, }); + if(!writerEntity) { + throw new Error('User Not Found') + } + const writer = new User(writerEntity); return { diff --git a/src/application/product/CreateProductCommentHandler.ts b/src/application/product/CreateProductCommentHandler.ts index 038b5e12..280151b1 100644 --- a/src/application/product/CreateProductCommentHandler.ts +++ b/src/application/product/CreateProductCommentHandler.ts @@ -45,6 +45,10 @@ export class CreateProductCommentHandler { }, }); + if(!writerEntity) { + throw new Error('User Not Found') + } + const writer = new User(writerEntity); return { diff --git a/src/application/user/UpdateUserProfileHandler.ts b/src/application/user/UpdateUserProfileHandler.ts index 5870dec7..60fa91e8 100644 --- a/src/application/user/UpdateUserProfileHandler.ts +++ b/src/application/user/UpdateUserProfileHandler.ts @@ -10,7 +10,7 @@ type TUpdateUserProfileUser = { } export class UpdateUserProfileHandler { - static async handle(requester: TUpdateUserProfileUser, { image }: { image: string | null }) { + static async handle(requester: TUpdateUserProfileUser, { image }: { image?: string | null }) { const userEntity = await prismaClient.user.findUnique({ where: { id: requester.userId, @@ -22,6 +22,7 @@ export class UpdateUserProfileHandler { const user = new User(userEntity); user.setImage(image); + await prismaClient.user.update({ where: { id: user.getId(), diff --git a/src/constant/ExceptionMessage.js b/src/constant/ExceptionMessage.ts similarity index 100% rename from src/constant/ExceptionMessage.js rename to src/constant/ExceptionMessage.ts diff --git a/src/domain/Article.js b/src/domain/Article.ts similarity index 80% rename from src/domain/Article.js rename to src/domain/Article.ts index 8ded4a65..ccc28de8 100644 --- a/src/domain/Article.js +++ b/src/domain/Article.ts @@ -1,3 +1,6 @@ +import { TArticleParam } from "@/types/article"; +import { TLikeParam } from "@/types/like"; + export class Article { /** ID */ _id; @@ -20,7 +23,10 @@ export class Article { /** 마지막 수정시각 */ _updatedAt; - constructor(param) { + /** 좋아요 수 **/ + _likes: TLikeParam[]; + + constructor(param: TArticleParam) { this._id = param.id; this._writerId = param.writerId; this._title = param.title; @@ -28,7 +34,7 @@ export class Article { this._image = param.image; this._createdAt = param.createdAt; this._updatedAt = param.updatedAt; - this._likes = params.likes; + this._likes = param.likes ?? []; } getId() { @@ -59,7 +65,7 @@ export class Article { return this._updatedAt; } - getIsFavorite(userId) { + getIsFavorite(userId: number) { if (!userId) return false; return this._likes.some((like) => like.userId === userId); diff --git a/src/domain/Comment.js b/src/domain/Comment.ts similarity index 91% rename from src/domain/Comment.js rename to src/domain/Comment.ts index 5642ab8b..0f88558f 100644 --- a/src/domain/Comment.js +++ b/src/domain/Comment.ts @@ -1,3 +1,5 @@ +import { TCommentParam } from "@/types/comment"; + export class Comment { /** ID */ _id; @@ -20,7 +22,7 @@ export class Comment { /** 마지막 수정시각 */ _updatedAt; - constructor(param) { + constructor(param: TCommentParam) { this._id = param.id; this._writerId = param.writerId; this._articleId = param.articleId; diff --git a/src/domain/Like.js b/src/domain/Like.ts similarity index 89% rename from src/domain/Like.js rename to src/domain/Like.ts index b0566712..f61b2d0d 100644 --- a/src/domain/Like.js +++ b/src/domain/Like.ts @@ -1,3 +1,5 @@ +import { TLikeParam } from "@/types/like"; + export class Like { /** ID */ _id; @@ -14,7 +16,7 @@ export class Like { /** 생성시각 */ _createdAt; - constructor(param) { + constructor(param: TLikeParam) { this._id = param.id; this._userId = param.userId; this._productId = param.productId; diff --git a/src/domain/Product.js b/src/domain/Product.ts similarity index 93% rename from src/domain/Product.js rename to src/domain/Product.ts index d84a3d39..9812ac69 100644 --- a/src/domain/Product.js +++ b/src/domain/Product.ts @@ -1,3 +1,5 @@ +import { TProductParam } from "@/types/product"; + export class Product { /** ID */ _id; @@ -29,7 +31,7 @@ export class Product { /** 좋아요 목록 */ _likes; - constructor(param) { + constructor(param : TProductParam) { this._id = param.id; this._ownerId = param.ownerId; this._name = param.name; @@ -78,7 +80,7 @@ export class Product { return this._updatedAt; } - getIsFavorite(userId) { + getIsFavorite(userId: number) { if (!userId) return false; return this._likes.some((like) => like.userId === userId); diff --git a/src/domain/User.js b/src/domain/User.ts similarity index 83% rename from src/domain/User.js rename to src/domain/User.ts index 85357607..2c58f6e1 100644 --- a/src/domain/User.js +++ b/src/domain/User.ts @@ -1,3 +1,5 @@ +import { TUserParam } from "@/types/user"; + export class User { /** ID */ _id; @@ -20,7 +22,7 @@ export class User { /** 마지막 수정시각 */ _updatedAt; - constructor(param) { + constructor(param: TUserParam) { this._id = param.id; this._email = param.email; this._password = param.password; @@ -54,15 +56,15 @@ export class User { return this._updatedAt; } - setImage(image) { + setImage(image?: string | null) { this._image = image; } - setPassword(password) { + setPassword(password: string) { this._password = password; } - checkPassword(password) { + checkPassword(password: string) { return this._password === password; } } diff --git a/src/exceptions/BadRequestException.js b/src/exceptions/BadRequestException.ts similarity index 81% rename from src/exceptions/BadRequestException.js rename to src/exceptions/BadRequestException.ts index 5c27518b..0c1243d1 100644 --- a/src/exceptions/BadRequestException.js +++ b/src/exceptions/BadRequestException.ts @@ -1,7 +1,7 @@ import { HttpException } from './HttpException.js'; export class BadRequestException extends HttpException { - constructor(name, message) { + constructor(name: string, message: string) { super({ status: 400, name, diff --git a/src/exceptions/ForbiddenException.js b/src/exceptions/ForbiddenException.ts similarity index 81% rename from src/exceptions/ForbiddenException.js rename to src/exceptions/ForbiddenException.ts index d0508962..66003790 100644 --- a/src/exceptions/ForbiddenException.js +++ b/src/exceptions/ForbiddenException.ts @@ -1,7 +1,7 @@ import { HttpException } from './HttpException.js'; export class ForbiddenException extends HttpException { - constructor(name, message) { + constructor(name: string, message: string) { super({ status: 403, name, diff --git a/src/exceptions/HttpException.js b/src/exceptions/HttpException.ts similarity index 77% rename from src/exceptions/HttpException.js rename to src/exceptions/HttpException.ts index 47e75836..fffd47cb 100644 --- a/src/exceptions/HttpException.js +++ b/src/exceptions/HttpException.ts @@ -6,11 +6,19 @@ * * @see asyncErrorHandler */ + +type HttpExceptionParam = { + status: number; + name: string; + message: string; +}; + + export class HttpException extends Error { status; name; - constructor(param) { + constructor(param: HttpExceptionParam) { const { status, name, message } = param; super(message); this.status = status; diff --git a/src/exceptions/InternalServerErrorException.js b/src/exceptions/InternalServerErrorException.ts similarity index 81% rename from src/exceptions/InternalServerErrorException.js rename to src/exceptions/InternalServerErrorException.ts index ee3ca246..a84fdedc 100644 --- a/src/exceptions/InternalServerErrorException.js +++ b/src/exceptions/InternalServerErrorException.ts @@ -1,7 +1,7 @@ import { HttpException } from './HttpException.js'; export class InternalServerErrorException extends HttpException { - constructor(name, message) { + constructor(name: string, message: string) { super({ status: 500, name, diff --git a/src/exceptions/NotFoundException.js b/src/exceptions/NotFoundException.ts similarity index 80% rename from src/exceptions/NotFoundException.js rename to src/exceptions/NotFoundException.ts index 29c62c88..2aff0da9 100644 --- a/src/exceptions/NotFoundException.js +++ b/src/exceptions/NotFoundException.ts @@ -1,7 +1,7 @@ import { HttpException } from './HttpException.js'; export class NotFoundException extends HttpException { - constructor(name, message) { + constructor(name: string, message: string) { super({ status: 404, name, diff --git a/src/exceptions/UnprocessableEntityException.js b/src/exceptions/UnprocessableEntityException.ts similarity index 81% rename from src/exceptions/UnprocessableEntityException.js rename to src/exceptions/UnprocessableEntityException.ts index bbd40adc..b24539d9 100644 --- a/src/exceptions/UnprocessableEntityException.js +++ b/src/exceptions/UnprocessableEntityException.ts @@ -1,7 +1,7 @@ import { HttpException } from './HttpException.js'; export class UnprocessableEntityException extends HttpException { - constructor(name, message) { + constructor(name: string, message: string) { super({ status: 422, name, diff --git a/src/types/article.ts b/src/types/article.ts index 55cb88fc..6f0db9aa 100644 --- a/src/types/article.ts +++ b/src/types/article.ts @@ -1,4 +1,4 @@ -import { Prisma } from "@prisma/client"; +import { TLikeParam } from "./like"; export type TArticleUser = { userId: number; @@ -14,4 +14,15 @@ export type TArticle = { take?: number | undefined; orderBy? : 'favorite' | 'recent'; keyword? : string | undefined; +} + +export type TArticleParam = { + id: number; + writerId: number; + title: string; + content: string; + image?: string | null; + createdAt: Date; + updatedAt: Date; + likes?: TLikeParam[] | null; } \ No newline at end of file diff --git a/src/types/comment.ts b/src/types/comment.ts new file mode 100644 index 00000000..50758053 --- /dev/null +++ b/src/types/comment.ts @@ -0,0 +1,9 @@ +export type TCommentParam = { + id: number; + writerId: number; + articleId?: number | null; + productId?: number | null; + content: string; + createdAt: Date; + updatedAt: Date; +} \ No newline at end of file diff --git a/src/types/like.ts b/src/types/like.ts new file mode 100644 index 00000000..28fd27de --- /dev/null +++ b/src/types/like.ts @@ -0,0 +1,7 @@ +export type TLikeParam = { + id: number; + userId: number; + productId?: number; + articleId?: number; + createdAt?: Date; + } \ No newline at end of file diff --git a/src/types/product.ts b/src/types/product.ts index a798543a..e838452c 100644 --- a/src/types/product.ts +++ b/src/types/product.ts @@ -1,4 +1,5 @@ import { Prisma } from "@prisma/client"; +import { TLikeParam } from "./like"; export type TProductUser = { userId: number; @@ -12,4 +13,17 @@ export type TProduct = { price: number; tags?: string[] | undefined; images : string[] | Prisma.ProductCreateimagesInput | undefined; +} + +export type TProductParam = { + name: string; + id: number; + createdAt: Date; + updatedAt: Date; + description: string; + price: number; + tags: string[]; + images: string[]; + ownerId: number; + likes?: TLikeParam[]; } \ No newline at end of file diff --git a/src/types/user.ts b/src/types/user.ts new file mode 100644 index 00000000..931c4fbe --- /dev/null +++ b/src/types/user.ts @@ -0,0 +1,9 @@ +export type TUserParam = { + id: number; + email: string; + password: string; + nickname: string; + image?: string | null; + createdAt: Date; + updatedAt: Date; +} \ No newline at end of file From 9f858db2496c85dffdcbc1f0f46dbed9de6ccf57 Mon Sep 17 00:00:00 2001 From: Sue Date: Fri, 20 Jun 2025 20:12:39 +0900 Subject: [PATCH 3/3] =?UTF-8?q?import=20path=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/exceptions/BadRequestException.ts | 2 +- src/exceptions/ForbiddenException.ts | 2 +- src/exceptions/InternalServerErrorException.ts | 2 +- src/exceptions/NotFoundException.ts | 2 +- src/exceptions/UnprocessableEntityException.ts | 2 +- src/interface/utils/asyncErrorHandler.ts | 6 +++--- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/exceptions/BadRequestException.ts b/src/exceptions/BadRequestException.ts index 0c1243d1..0423f69c 100644 --- a/src/exceptions/BadRequestException.ts +++ b/src/exceptions/BadRequestException.ts @@ -1,4 +1,4 @@ -import { HttpException } from './HttpException.js'; +import { HttpException } from './HttpException'; export class BadRequestException extends HttpException { constructor(name: string, message: string) { diff --git a/src/exceptions/ForbiddenException.ts b/src/exceptions/ForbiddenException.ts index 66003790..40c070cf 100644 --- a/src/exceptions/ForbiddenException.ts +++ b/src/exceptions/ForbiddenException.ts @@ -1,4 +1,4 @@ -import { HttpException } from './HttpException.js'; +import { HttpException } from './HttpException'; export class ForbiddenException extends HttpException { constructor(name: string, message: string) { diff --git a/src/exceptions/InternalServerErrorException.ts b/src/exceptions/InternalServerErrorException.ts index a84fdedc..42581183 100644 --- a/src/exceptions/InternalServerErrorException.ts +++ b/src/exceptions/InternalServerErrorException.ts @@ -1,4 +1,4 @@ -import { HttpException } from './HttpException.js'; +import { HttpException } from './HttpException'; export class InternalServerErrorException extends HttpException { constructor(name: string, message: string) { diff --git a/src/exceptions/NotFoundException.ts b/src/exceptions/NotFoundException.ts index 2aff0da9..1fde801a 100644 --- a/src/exceptions/NotFoundException.ts +++ b/src/exceptions/NotFoundException.ts @@ -1,4 +1,4 @@ -import { HttpException } from './HttpException.js'; +import { HttpException } from './HttpException'; export class NotFoundException extends HttpException { constructor(name: string, message: string) { diff --git a/src/exceptions/UnprocessableEntityException.ts b/src/exceptions/UnprocessableEntityException.ts index b24539d9..0ce6068a 100644 --- a/src/exceptions/UnprocessableEntityException.ts +++ b/src/exceptions/UnprocessableEntityException.ts @@ -1,4 +1,4 @@ -import { HttpException } from './HttpException.js'; +import { HttpException } from '../exceptions/HttpException'; export class UnprocessableEntityException extends HttpException { constructor(name: string, message: string) { diff --git a/src/interface/utils/asyncErrorHandler.ts b/src/interface/utils/asyncErrorHandler.ts index caf67b67..7e2c6de9 100644 --- a/src/interface/utils/asyncErrorHandler.ts +++ b/src/interface/utils/asyncErrorHandler.ts @@ -1,8 +1,8 @@ import superstruct from 'superstruct'; -import { HttpException } from '../../exceptions/HttpException.js'; -import { BadRequestException } from '../../exceptions/BadRequestException.js'; -import { InternalServerErrorException } from '../../exceptions/InternalServerErrorException.js'; +import { HttpException } from '../../exceptions/HttpException'; +import { BadRequestException } from '../../exceptions/BadRequestException'; +import { InternalServerErrorException } from '../../exceptions/InternalServerErrorException'; import { Request, Response } from 'express'; export function asyncErrorHandler(handler: any) {