diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index 4c04c55b..4d12b85f 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -12,7 +12,7 @@ on: jobs: build_api: - if: github.event.pull_request.merged == true + if: ${{ github.event.pull_request.merged == true && contains(github.ref, 'dev-be')}} runs-on: ubuntu-latest steps: - name: 체크아웃 @@ -49,7 +49,7 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_ASUMI_URL }} if: always() build_socket: - if: github.event.pull_request.merged == true + if: ${{ github.event.pull_request.merged == true && contains(github.ref, 'dev-be')}} runs-on: ubuntu-latest steps: - name: 체크아웃 @@ -85,7 +85,7 @@ jobs: cache: 'npm' - name: Yarn 설치 - run: npm install yarn + run: npm install yarn --force - name: yarn으로 패키지 설치 run: yarn install @@ -109,7 +109,7 @@ jobs: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_FAIL_WEBHOOK_URL }} if: failure() nCloudDeploy: - if: github.event.pull_request.merged == true + if: ${{ github.event.pull_request.merged == true && contains(github.ref, 'dev-be')}} needs: [build_api, build_socket] runs-on: ubuntu-latest steps: diff --git a/README.md b/README.md index 0c778a84..9384d4d8 100644 --- a/README.md +++ b/README.md @@ -30,16 +30,29 @@ ## 프로젝트 소개 아스니티(Asmi + Community)는 디스코드와 슬랙을 참고한 실시간 채팅 서비스입니다. -실시간 통신을 통해 커뮤니티에 속한 사용자들끼리 채팅할 수 있으며, 귤 까먹으면서 채팅(시간제한이 있는 채팅)을 할 수 있는 기능이 있습니다. +실시간 통신을 통해 커뮤니티에 속한 사용자들끼리 채팅할 수 있습니다. + +## 프로젝스 소개 자료 + +📎 [배포 링크](http://www.asnity.site) : http://www.asnity.site + +🖥 [데모 영상](https://www.youtube.com/watch?v=2gI3OlJXAZQ) + +📽 [발표 영상](https://youtu.be/vEL2TTPZ9tk) + +📋 [발표 PPT](https://docs.google.com/presentation/d/1kduK9v3o7nCGQghsplh-WrS9VVjWvp8R/edit?usp=sharing&ouid=115620821189866783380&rtpof=true&sd=true) ## Wiki +- [Team Notion](https://grand-beanie-e57.notion.site/Asnity-cbd4dcce58f540b4b5b7ff33c8cea984) - [기획서](https://github.com/boostcampwm-2022/web24-Asnity/wiki/%EA%B8%B0%ED%9A%8D%EC%84%9C) - [Architecture](https://github.com/boostcampwm-2022/web24-Asnity/wiki/Architecture) - [Skill Spec](https://github.com/boostcampwm-2022/web24-Asnity/wiki/Skill-Spec) - [Database ERD](https://github.com/boostcampwm-2022/web24-Asnity/wiki/DB-Diagram) - [Backlog](https://lake-duke-f63.notion.site/25c4c9e46d464ea1a82a68c8399ceaf0?v=ea1b4f77e71f4d17b2be0ebdc9c03702) - + + + ## Skill Spec
@@ -52,7 +65,7 @@
- +
@@ -65,4 +78,4 @@
-![stack](https://user-images.githubusercontent.com/79135734/206748158-d0659242-4034-4cf3-bbab-95418d49bf54.PNG) +![image](https://user-images.githubusercontent.com/34162358/207605404-3da6f4f9-65a6-4167-992a-6eef41ccebd9.png) diff --git a/client/config/webpack.analysis.ts b/client/config/webpack.analysis.ts index e23525fa..cf599142 100644 --- a/client/config/webpack.analysis.ts +++ b/client/config/webpack.analysis.ts @@ -9,7 +9,7 @@ import { merge } from 'webpack-merge'; import common from './webpack.common'; const config: Configuration = { - devtool: 'inline-source-map', + devtool: 'cheap-module-source-map', mode: 'production', module: { rules: [ diff --git a/client/config/webpack.prod.ts b/client/config/webpack.prod.ts index 1be3571e..1597826c 100644 --- a/client/config/webpack.prod.ts +++ b/client/config/webpack.prod.ts @@ -8,7 +8,7 @@ import { merge } from 'webpack-merge'; import common from './webpack.common'; const config: Configuration = { - devtool: 'inline-source-map', + devtool: 'cheap-module-source-map', mode: 'production', module: { rules: [ diff --git a/client/package.json b/client/package.json index 6e10ac17..6a4831c8 100644 --- a/client/package.json +++ b/client/package.json @@ -7,14 +7,15 @@ "dev": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.dev.ts --progress", "dev:proxy": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.dev.proxy.ts --progress", "dev:https": "cross-env NODE_ENV=development webpack serve --config ./config/webpack.dev.https.ts --progress", - "build:dev": "cross-env NODE_ENV=production webpack --config ./config/webpack.prod:dev.ts --progress", "build": "cross-env NODE_ENV=production webpack --config ./config/webpack.prod.ts --progress", "analysis": "cross-env NODE_ENV=production webpack --config ./config/webpack.analysis.ts --progress", "test": "jest" }, "dependencies": { "@heroicons/react": "^2.0.13", + "@loadable/component": "^5.15.2", "@tanstack/react-query": "^4.16.1", + "@types/loadable__component": "^5.13.4", "axios": "^1.1.3", "classnames": "^2.3.2", "immer": "^9.0.16", diff --git a/client/public/index.html b/client/public/index.html index 84305a91..40e14bcc 100644 --- a/client/public/index.html +++ b/client/public/index.html @@ -11,6 +11,7 @@ Asnity +
diff --git a/client/src/App.tsx b/client/src/App.tsx index 5dd3ee04..dd9a347d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,20 +1,7 @@ import ChannelLayer from '@layouts/ChannelLayer'; import CommunityLayer from '@layouts/CommunityLayer'; -import AccessDenied from '@pages/AccessDenied'; -import AuthorizedLayer from '@pages/AuthorizedLayer'; -import Channel from '@pages/Channel'; -import Community from '@pages/Community'; -import DM from '@pages/DM'; -import DMRoom from '@pages/DMRoom'; -import Friends from '@pages/Friends'; -import Home from '@pages/Home'; -import NotFound from '@pages/NotFound'; -import Root from '@pages/Root'; -import SignIn from '@pages/SignIn'; -import SignUp from '@pages/SignUp'; -import UnAuthorizedLayer from '@pages/UnAuthorizedLayer'; -import UnknownError from '@pages/UnknownError'; -import communitiesLoader from '@routes/communitiesLoader'; +import loadable from '@loadable/component'; +import HomeErrorElement from '@routes/HomeErrorElement'; import React from 'react'; import { RouterProvider, @@ -25,18 +12,27 @@ import { Outlet, } from 'react-router-dom'; -import queryClient from './queryClient'; +const AccessDenied = loadable(() => import('@pages/AccessDenied')); +const AuthorizedLayer = loadable(() => import('@pages/AuthorizedLayer')); +const Channel = loadable(() => import('@pages/Channel')); +const Community = loadable(() => import('@pages/Community')); +const DM = loadable(() => import('@pages/DM')); +const DMRoom = loadable(() => import('@pages/DMRoom')); +const Error = loadable(() => import('@pages/Error')); +const Friends = loadable(() => import('@pages/Friends')); +const Home = loadable(() => import('@pages/Home')); +const NotFound = loadable(() => import('@pages/NotFound')); +const Root = loadable(() => import('@pages/Root')); +const SignIn = loadable(() => import('@pages/SignIn')); +const SignUp = loadable(() => import('@pages/SignUp')); +const UnAuthorizedLayer = loadable(() => import('@pages/UnAuthorizedLayer')); const router = createBrowserRouter( createRoutesFromElements( } /> }> - } - loader={communitiesLoader(queryClient)} - errorElement={} - > + } errorElement={}> }> } /> } /> } /> - } /> + } /> } /> , ), diff --git a/client/src/apis/channel.ts b/client/src/apis/channel.ts index 5dfcfc1e..802e558b 100644 --- a/client/src/apis/channel.ts +++ b/client/src/apis/channel.ts @@ -9,7 +9,7 @@ export interface JoinedChannel { name: string; isPrivate: boolean; description: string; - lastRead: boolean; // NOTE: get communities에는 있는데, get channel에는 없는 프로퍼티. + existUnreadChat: boolean; // NOTE: get communities에는 있는데, get channel에는 없는 프로퍼티. type: string; // TODO: DM or Channel -> DM 구현할 때 타입 구체화 createdAt: string; } @@ -76,7 +76,7 @@ export const leaveChannel: LeaveChannel = (channelId) => { return tokenAxios .delete(endPoint) - .then((res) => res.data.result); + .then((response) => response.data.result); }; export interface InviteChannelRequest { @@ -106,3 +106,26 @@ export const inviteChannel: InviteChannel = ({ }) .then((response) => response.data.result); }; + +export interface UpdateLastReadRequest { + channelId: string; + communityId: string; +} + +export interface UpdateLastReadResult { + message: string; +} +export type UpdateLastReadResponse = SuccessResponse; +export type UpdateLastRead = ( + fields: UpdateLastReadRequest, +) => Promise; + +export const updateLastRead: UpdateLastRead = ({ channelId, communityId }) => { + const endPoint = `/api/channels/${channelId}/lastRead`; + + return tokenAxios + .patch(endPoint, { + community_id: communityId, + }) + .then((response) => response.data.result); +}; diff --git a/client/src/apis/chat.ts b/client/src/apis/chat.ts index 168cb68a..41e39d0e 100644 --- a/client/src/apis/chat.ts +++ b/client/src/apis/chat.ts @@ -3,10 +3,10 @@ import type { SuccessResponse } from '@@types/apis/response'; import endPoint from '@constants/endPoint'; import { tokenAxios } from '@utils/axios'; -export type ChatType = 'TEXT' | 'IMAGE'; +export type ChatType = 'TEXT' | 'IMAGE' | 'SYSTEM'; export interface Chat { - id: string; + id: number; // TODO: chatId로 바꿔야 할 수도. type: ChatType; content: string; senderId: string; @@ -37,3 +37,20 @@ export const getChats: GetChats = (channelId, prev) => { .get(_endPoint, { params: { prev } }) .then((response) => response.data.result); }; + +export type GetUnreadChatIdResult = { + unreadChatId: number; +}; + +export type GetUnreadChatIdResponse = SuccessResponse; +export type GetUnreadChatId = ( + channelId: string, +) => Promise; + +export const getUnreadChatId: GetUnreadChatId = (channelId) => { + const _endPoint = endPoint.getUnreadChatId(channelId); + + return tokenAxios + .get(_endPoint) + .then((response) => response.data.result.unreadChatId); +}; diff --git a/client/src/apis/dm.ts b/client/src/apis/dm.ts index 44bae2e2..7cba0ac9 100644 --- a/client/src/apis/dm.ts +++ b/client/src/apis/dm.ts @@ -24,5 +24,5 @@ export const getDirectMessages: GetDirectMessages = () => { return tokenAxios .get(endPoint) - .then((res) => res.data.result); + .then((response) => response.data.result); }; diff --git a/client/src/apis/user.ts b/client/src/apis/user.ts index 1ee825a3..d032529f 100644 --- a/client/src/apis/user.ts +++ b/client/src/apis/user.ts @@ -40,25 +40,25 @@ export const getFollowings: GetFollowings = () => { return tokenAxios .get(endPoint) - .then((res) => res.data.result.followings); + .then((response) => response.data.result.followings); }; -export interface UpdateFollowingResult { +export interface ToggleFollowingResult { message?: string; } -export type UpdateFollowingResponse = SuccessResponse; -export type UpdateFollowing = ( +export type ToggleFollowingResponse = SuccessResponse; +export type ToggleFollowing = ( userId: string, -) => Promise; +) => Promise; // 유저를 팔로우한다. // 유저가 팔로잉 상태라면 언팔로우 한다. (toggle) -export const updateFollowing: UpdateFollowing = (userId) => { +export const toggleFollowing: ToggleFollowing = (userId) => { const endPoint = `/api/user/following/${userId}`; return tokenAxios - .post(endPoint) - .then((res) => res.data.result); + .post(endPoint) + .then((response) => response.data.result); }; export type GetFollowersResult = { @@ -72,7 +72,7 @@ export const getFollowers: GetFollowers = () => { return tokenAxios .get(endPoint) - .then((res) => res.data.result.followers); + .then((response) => response.data.result.followers); }; export interface GetUsersParams { diff --git a/client/src/components/Avatar/index.tsx b/client/src/components/Avatar/index.tsx index c72b4137..21433388 100644 --- a/client/src/components/Avatar/index.tsx +++ b/client/src/components/Avatar/index.tsx @@ -1,21 +1,11 @@ +import type { User } from '@apis/user'; import type { USER_STATUS } from '@constants/user'; import type { ReactNode, FC } from 'react'; +import { ASNITY_DEVELOPER } from '@constants/user'; import React, { memo } from 'react'; -type BadgeType = keyof typeof USER_STATUS; - -export interface Props { - name: string; - size?: 'sm' | 'md'; - variant?: 'circle' | 'rectangle'; - profileUrl?: string; - className?: string; - children?: ReactNode; - status?: BadgeType; - badge?: boolean; - badgePosition?: 'top-right' | 'bottom-right' | 'top-left' | 'bottom-left'; -} +type BadgeType = keyof typeof USER_STATUS | 'NEW'; const ROUNDED = { rectangle: 'rounded-2xl', @@ -49,9 +39,24 @@ const BADGE_COLOR: Record = { ONLINE: 'bg-success', AFK: 'bg-error', OFFLINE: 'bg-label', + NEW: 'bg-indigo', }; +export interface Props { + name: string; + size?: 'sm' | 'md'; + variant?: 'circle' | 'rectangle'; + profileUrl?: string; + className?: string; + children?: ReactNode; + status?: BadgeType; + badge?: boolean; + badgePosition?: 'top-right' | 'bottom-right' | 'top-left' | 'bottom-left'; + user?: User; +} + const Avatar: FC = ({ + user, name, profileUrl, size = 'sm', @@ -62,6 +67,13 @@ const Avatar: FC = ({ badgePosition = 'bottom-right', status = 'OFFLINE', }) => { + const isSuperUser = user && ASNITY_DEVELOPER[user.id]; + const _profileUrl = isSuperUser + ? ASNITY_DEVELOPER[user.id] + : profileUrl === 'url' + ? ASNITY_DEVELOPER.default + : profileUrl; + return (
{badge && ( @@ -74,14 +86,10 @@ const Avatar: FC = ({ > {children} {!children && - (profileUrl ? ( + (_profileUrl ? ( {`${name}의 ) : ( diff --git a/client/src/components/ChannelCard/index.tsx b/client/src/components/ChannelCard/index.tsx new file mode 100644 index 00000000..96dd8089 --- /dev/null +++ b/client/src/components/ChannelCard/index.tsx @@ -0,0 +1,79 @@ +import type { JoinedChannel } from '@apis/channel'; +import type { User } from '@apis/user'; +import type { FC } from 'react'; + +import Avatar from '@components/Avatar'; +import { + CalendarDaysIcon, + LinkIcon, + LockClosedIcon, +} from '@heroicons/react/24/solid'; +import { dateStringToKRLocaleDateString } from '@utils/date'; +import cn from 'classnames'; +import React from 'react'; +import { Link } from 'react-router-dom'; + +export interface Props { + channel: JoinedChannel; + manager: User; +} + +const ChannelCard: FC = ({ channel, manager }) => { + const { isPrivate, _id, name, description, createdAt } = channel; + const iconContainerClassnames = cn({ + 'bg-label': !isPrivate, + 'bg-indigo': isPrivate, + }); + + return ( +
  • +
    + + {isPrivate ? ( + + ) : ( + + )} + +
    + +
    +
    {name}
    +
    {description}
    +
    + + {manager.nickname} (관리자) +
    +
    +
    + 생성 날짜 + +
    +
    + {dateStringToKRLocaleDateString(createdAt, { + hour: 'numeric', + minute: 'numeric', + })} + 에 만들어진{' '} + + {isPrivate && '비공개'} + + 채널입니다. +
    +
    +
    +
  • + ); +}; + +export default ChannelCard; diff --git a/client/src/components/ChannelContextMenu/index.tsx b/client/src/components/ChannelContextMenu/index.tsx index 16786ed0..08b01ab8 100644 --- a/client/src/components/ChannelContextMenu/index.tsx +++ b/client/src/components/ChannelContextMenu/index.tsx @@ -39,7 +39,7 @@ const ChannelContextMenu: FC = ({ channel }) => { }); }; - const handleClickChannelSettingsButton = () => { }; + const handleClickChannelSettingsButton = () => {}; const handleClickChannelLeaveButton = () => { closeContextMenuModal(); @@ -58,7 +58,7 @@ const ChannelContextMenu: FC = ({ channel }) => {

    채널 컨텍스트 메뉴

      - {channel.isPrivate && ( + {
    • - )} + }