이어서 할 곳

GitHub - fc-micro-frontends/career-up at step10

REACT_APP_AUTH0_DOMAIN=dev-vcrmf0xuep020tri.us.auth0.com
REACT_APP_AUTH0_CLIENT_ID=27LLb4I9cIjiiQNSjbNIX9sQM1KECUfk
REACT_APP_AUTH0_CALLBACK_URL=http://localhost:3000
➜ pnpm i

➜ pnpm build

API 코드 작성

// career-up/apps/edu/src/types.ts

import { type User } from "@auth0/auth0-spa-js";

export interface UserType extends User {
  view_count: number;
  update_count: number;
  courses: { courseId: number; done: false }[];
}

export interface CourseType {
  id: number;
  thumbnail: string;
  title: string;
  description: string;
}

export interface CourseContentsType {
  id: number;
  goals: string[];
  summaries: string[];
}
// career-up/apps/edu/src/apis.ts

import {
  type CourseContentsType,
  type CourseType,
  type UserType,
} from "./types";

export async function getCourses(token: string): Promise<CourseType[]> {
  const res = await fetch(
    "<http://localhost:4000/courses?_sort=id&_order=desc>",
    {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    }
  );

  return await res.json();
}

export async function getCourseContents(
  token: string,
  id: number
): Promise<CourseContentsType> {
  const res = await fetch(`http://localhost:4000/course-contents/${id}`, {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });

  return await res.json();
}

export async function getUser(token: string): Promise<UserType> {
  const res = await fetch("<http://localhost:4000/user>", {
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });

  return await res.json();
}

공통 Layout 처리

// career-up/apps/edu/src/routes.tsx

import React from "react";
import { type RouteObject } from "react-router-dom";
import { AppRoutingManager } from "@career-up/shell-router";
import Auth0ClientProvider from "./providers/auth0-client-provider";
**import Layout from "./components/layout";**

export const routes: RouteObject[] = [
  {
    path: "/",
    element: (
      <Auth0ClientProvider>
        **<Layout>**
          <AppRoutingManager type="app-edu" />
        **</Layout>**
      </Auth0ClientProvider>
    ),
    errorElement: <div>App Edu Error</div>,
    **children: [
      {
        index: true,
        element: <div>PageList</div>,
      },
      {
        path: ":id",
        element: <div>PageDetail</div>,
      },
    ],**
  },
];
// career-up/apps/edu/src/components/layout.styles.ts

import styled from "@emotion/styled";

export const LayoutWrapper = styled.div`
  display: flex;
  flex-direction: row;
  gap: 24px;
  margin: 0 auto;
  max-width: 1128px;
  padding: 16px;

  .edu--layout-left {
    display: flex;
    flex-direction: column;
    width: 225px;
    gap: 10px;
  }

  .edu--layout-center {
    display: flex;
    flex-direction: column;
    width: 879px;
    gap: 10px;
  }
`;
// career-up/apps/edu/src/atoms.ts

import { atom } from "jotai";
import { type CourseType, type UserType } from "./types";

export const userAtom = atom<UserType | null>(null);

export const coursesAtom = atom<CourseType[]>([]);
// career-up/apps/edu/src/components/layout.tsx

import React, { useEffect } from "react";
import { LayoutWrapper } from "./layout.styles";
import { useSetAtom } from "jotai";
import { coursesAtom, userAtom } from "../atoms";
import useAuth0Client from "../hooks/use-auth0-client";
import { getCourses, getUser } from "../apis";
import ProfileContainer from "../containers/profile-container";
import MyCourseInfoContainer from "../containers/my-course-info-container";

const Layout: React.FC<React.PropsWithChildren> = ({ children }) => {
  const auth0Client = useAuth0Client();
  const setUser = useSetAtom(userAtom);
  const setCourses = useSetAtom(coursesAtom);

  useEffect(() => {
    (async () => {
      try {
        const token = await auth0Client.getTokenSilently();
        getUser(token).then(setUser);
        getCourses(token).then(setCourses);
      } catch (error) {
        alert(error);
      }
    })();
  }, [auth0Client, setCourses, setUser]);

  return (
    <LayoutWrapper>
      <div className="edu--layout-left">
        <ProfileContainer />
        <MyCourseInfoContainer />
      </div>
      <div className="edu--layout-center">{children}</div>
    </LayoutWrapper>
  );
};

export default Layout;
// career-up/apps/edu/src/components/profile.styles.ts

import styled from "@emotion/styled";

export const ProfileWrapper = styled.div`
  .edu--profile-top {
    display: flex;
    flex-direction: column;
    background-color: white;

    padding: 30px 12px 16px;
    border-radius: 8px 8px 0 0;
    border-bottom: 1px solid rgb(0 0 0 / 0.1);
    gap: 10px;
    justify-content: center;
    align-items: center;

    img {
      width: 50px;
    }

    .edu--profile-name {
      font-size: 16px;
      font-weight: bold;
    }

    .edu--profile-email {
      color: rgb(0 0 0/0.6);
      font-size: 12px;
    }
  }

  .edu--profile-bottom {
    display: flex;
    flex-direction: column;
    padding: 16px 12px 16px;
    border-radius: 0 0 8px 8px;
    background-color: white;
    gap: 10px;

    .edu--profile-bottom-item {
      display: flex;
      flex-direction: row;
      justify-content: space-between;

      color: rgb(0 0 0/0.6);
      font-size: 14px;

      .edu--profile-bottom-item-count {
        color: #0a66c2;
        cursor: pointer;
      }
    }
  }
`;
// career-up/apps/edu/src/components/profile.tsx

import React from "react";
import { ProfileWrapper } from "./profile.styles";
import { type UserType } from "../types";

interface ProfileProps {
  user: UserType | null;
}

const Profile: React.FC<ProfileProps> = ({ user }) => {
  if (user === null) {
    return null;
  }

  const { picture, name, email, view_count, update_count } = user;

  return (
    <ProfileWrapper>
      <div className="edu--profile-top">
        <img src={picture} />
        <div className="edu--profile-name">{name}</div>
        <div className="edu--profile-email">{email}</div>
      </div>
      <div className="edu--profile-bottom">
        <div className="edu--profile-bottom-item">
          <div>프로필 조회자</div>
          <div className="edu--profile-bottom-item-count">{view_count}</div>
        </div>
        <div className="edu--profile-bottom-item">
          <div>업데이트 노출</div>
          <div className="edu--profile-bottom-item-count">{update_count}</div>
        </div>
      </div>
    </ProfileWrapper>
  );
};

export default Profile;
// career-up/apps/edu/src/containers/profile-container.tsx

import { useAtomValue } from "jotai";
import React from "react";
import { userAtom } from "../atoms";
import Profile from "../components/profile";

const ProfileContainer: React.FC = () => {
  const user = useAtomValue(userAtom);

  return <Profile user={user} />;
};

export default ProfileContainer;
// career-up/apps/edu/src/components/my-course-info.styles.ts

import styled from "@emotion/styled";

export const MyCourseInfoWrapper = styled.div`
  .edu--my-course-info-top {
    display: flex;
    flex-direction: column;
    background-color: white;

    padding: 16px 12px 16px;
    border-radius: 8px 8px 0 0;
    border-bottom: 1px solid rgb(0 0 0 / 0.1);
    gap: 10px;

    .edu--my-course-info-top-title {
      font-size: 15px;
      font-weight: bold;
      height: 15px;
    }
  }

  .edu--my-course-info-bottom {
    display: flex;
    flex-direction: column;
    padding: 16px 12px 16px;
    border-radius: 0 0 8px 8px;
    background-color: white;
    gap: 10px;

    .edu--my-course-info-bottom-item {
      display: flex;
      flex-direction: row;
      justify-content: space-between;

      color: rgb(0 0 0/0.6);
      font-size: 14px;

      .edu--my-course-info-bottom-item-count {
        color: #0a66c2;
        cursor: pointer;
      }
    }
  }
`;
// career-up/apps/edu/src/components/my-course-info.tsx

import React from "react";
import { MyCourseInfoWrapper } from "./my-course-info.styles";
import { type UserType } from "../types";

interface MyCourseInfoProps {
  user: UserType | null;
}

const MyCourseInfo: React.FC<MyCourseInfoProps> = ({ user }) => {
  if (user === null) {
    return null;
  }

  const { courses } = user;

  return (
    <MyCourseInfoWrapper>
      <div className="edu--my-course-info-top">
        <span className="edu--my-course-info-top-title">나의 학습 현황</span>
      </div>
      <div className="edu--my-course-info-bottom">
        <div className="edu--my-course-info-bottom-item">
          <div>전체 수강 강좌</div>
          <div className="edu--my-course-info-bottom-item-count">
            {courses.length}
          </div>
        </div>
        <div className="edu--my-course-info-bottom-item">
          <div>수강 중인 강좌</div>
          <div className="edu--my-course-info-bottom-item-count">
            {courses.filter((course) => !course.done).length}
          </div>
        </div>
        <div className="edu--my-course-info-bottom-item">
          <div>수강 완료한 강좌</div>
          <div className="edu--my-course-info-bottom-item-count">
            {courses.filter((course) => course.done).length}
          </div>
        </div>
      </div>
    </MyCourseInfoWrapper>
  );
};

export default MyCourseInfo;
// career-up/apps/edu/src/containers/my-course-info-container.tsx

import React from "react";
import MyCourseInfo from "../components/my-course-info";
import { useAtomValue } from "jotai";
import { userAtom } from "../atoms";

const MyCourseInfoContainer: React.FC = () => {
  const user = useAtomValue(userAtom);

  return <MyCourseInfo user={user} />;
};

export default MyCourseInfoContainer;

리스트 페이지 작업

// career-up/apps/edu/src/pages/page-list.tsx

import React from "react";
import CourseListItem from "../components/course-list-item";
import { coursesAtom } from "../atoms";
import { useAtomValue } from "jotai";

const PageList: React.FC = () => {
  const courses = useAtomValue(coursesAtom);

  return (
    <>
      {courses.map((course) => (
        <CourseListItem key={course.id} {...course} />
      ))}
    </>
  );
};

export default PageList;