diff --git a/fixture/data.js b/fixture/data.js
index 15d1886..69b9f01 100644
--- a/fixture/data.js
+++ b/fixture/data.js
@@ -18,12 +18,12 @@ export const users = [
},
{
uid: 'Uid2',
- githubId: 'daadaadaah',
+ githubId: 'daadaadaah2',
githubProfile: 'https://avatars1.githubusercontent.com/u/60481383?s=460&v=4',
},
{
uid: 'Uid3',
- githubId: 'daadaadaah',
+ githubId: 'daadaadaah3',
githubProfile: 'https://avatars1.githubusercontent.com/u/60481383?s=460&v=4',
},
]
diff --git a/front-end/services/api/__mocks__/api.js b/front-end/services/api/__mocks__/api.js
index 180f309..fda1105 100644
--- a/front-end/services/api/__mocks__/api.js
+++ b/front-end/services/api/__mocks__/api.js
@@ -1,3 +1,5 @@
+import { user } from '../../../../fixture/data';
+
export async function getDevLinks() {
return [];
}
@@ -7,3 +9,7 @@ export async function getUsers() {
}
export const addUser = jest.fn();
+
+export async function getUser() {
+ return user;
+}
diff --git a/front-end/services/api/api.js b/front-end/services/api/api.js
index 4ef0b84..1fe444b 100644
--- a/front-end/services/api/api.js
+++ b/front-end/services/api/api.js
@@ -23,3 +23,9 @@ export async function addUser({ uid, githubId, githubProfile }) {
return resaddUserInfo;
}
+
+export async function getUser({ githubId }) {
+ const responses = await db.collection('user').where('githubId', '==', githubId).get();
+
+ return responses.docs.map((doc) => (doc.data()))[0];
+}
diff --git a/front-end/services/api/api.test.js b/front-end/services/api/api.test.js
index f9e9399..91967c7 100644
--- a/front-end/services/api/api.test.js
+++ b/front-end/services/api/api.test.js
@@ -1,4 +1,6 @@
-import { getDevLinks, getUsers, addUser } from './api';
+import {
+ getDevLinks, getUsers, addUser, getUser,
+} from './api';
import { devLinks, users, user } from '../../../fixture/data';
@@ -30,4 +32,12 @@ describe('api', () => {
expect(data).toEqual(user);
});
});
+
+ describe('getUser', () => {
+ it('returns user', async () => {
+ const data = await getUser({ githubId: user.githubId });
+
+ expect(data).toEqual(user);
+ });
+ });
});
diff --git a/front-end/services/firebase/__mocks__/firebase.js b/front-end/services/firebase/__mocks__/firebase.js
index d63d22f..1ed9a29 100644
--- a/front-end/services/firebase/__mocks__/firebase.js
+++ b/front-end/services/firebase/__mocks__/firebase.js
@@ -50,14 +50,35 @@ const firebase = {
data: () => item,
})),
}),
- where: jest.fn().mockImplementation(() => ({
- get: jest.fn().mockResolvedValue({
- docs: collections[name].map((item) => ({
- id: item.uid,
- data: () => item,
- })),
- }),
- })),
+ where: jest.fn().mockImplementation((fieldName, operator, value) => {
+ let result = null;
+
+ if (operator === '==') {
+ result = ({
+ get: jest.fn().mockResolvedValue({
+ docs: collections[name]
+ .filter((doc) => doc[fieldName] === value)
+ .map((doc) => ({
+ id: doc.uid,
+ data: () => doc,
+ })),
+ }),
+ });
+ }
+
+ if (operator === 'in') {
+ result = ({
+ get: jest.fn().mockResolvedValue({
+ docs: collections[name].map((item) => ({
+ id: item.uid,
+ data: () => item,
+ })),
+ }),
+ });
+ }
+
+ return result;
+ }),
doc: jest.fn().mockImplementation((docName) => ({
get: jest.fn().mockResolvedValue({
id: docName,
diff --git a/front-end/src/App.jsx b/front-end/src/App.jsx
index 8fac40f..baf9cd9 100644
--- a/front-end/src/App.jsx
+++ b/front-end/src/App.jsx
@@ -7,6 +7,7 @@ import { Switch, Route } from 'react-router-dom';
import Header from './Header';
import HomePage from './HomePage';
+import UserPage from './UserPage';
import NotFoundPage from './NotFoundPage';
const Wrapper = styled.div({
@@ -20,6 +21,7 @@ export default function App() {
+
diff --git a/front-end/src/App.test.jsx b/front-end/src/App.test.jsx
index 7a92f74..fc3f18b 100644
--- a/front-end/src/App.test.jsx
+++ b/front-end/src/App.test.jsx
@@ -4,10 +4,12 @@ import { render } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
-import { useDispatch } from 'react-redux';
+import { useDispatch, useSelector } from 'react-redux';
import App from './App';
+import { user } from '../../fixture/data';
+
jest.mock('react-redux');
jest.mock('../services/firebase/firebase.js');
@@ -16,6 +18,10 @@ describe('App with router', () => {
beforeEach(() => {
useDispatch.mockImplementation(() => dispatch);
+
+ useSelector.mockImplementation((selector) => selector({
+ devLinkerInfo: user,
+ }));
});
function renderApp({ path }) {
@@ -36,13 +42,22 @@ describe('App with router', () => {
});
context('with path /', () => {
- it('shows loading message', () => {
+ it('shows devlinks', () => {
const { container } = renderApp({ path: '/' });
expect(container).toHaveTextContent('로딩중');
});
});
+ context('with path /user/devlinker', () => {
+ it('shows devlinkerInfo', () => {
+ const { container, getByAltText } = renderApp({ path: '/user/devlinker' });
+
+ expect(getByAltText(`${user.githubId}`)).toHaveAttribute('src', user.githubProfile);
+ expect(container).toHaveTextContent(user.githubId);
+ });
+ });
+
context('with invalid path', () => {
it('renders the not found page', () => {
const { container } = renderApp({ path: '/xxx' });
diff --git a/front-end/src/Header.jsx b/front-end/src/Header.jsx
index b77677e..8895b73 100644
--- a/front-end/src/Header.jsx
+++ b/front-end/src/Header.jsx
@@ -90,7 +90,9 @@ export default function Header() {
{userInfo ? (
-
+
+
+
diff --git a/front-end/src/UserInfo.jsx b/front-end/src/UserInfo.jsx
new file mode 100644
index 0000000..f21a6aa
--- /dev/null
+++ b/front-end/src/UserInfo.jsx
@@ -0,0 +1,63 @@
+import React from 'react';
+
+import styled from '@emotion/styled';
+
+const Wrapper = styled.div({
+ height: '100%',
+ padding: '0px 50px',
+ display: 'flex',
+ flexDirection: 'column',
+ margin: '0 10em',
+});
+const TopWrapper = styled.div({
+ display: 'flex',
+ flexDirection: 'row',
+ borderBottom: '#dcdc 1px solid',
+
+});
+
+const TopLeftWrapper = styled.div({
+ padding: '0 4em 4em 4em',
+});
+
+const ImgWrapper = styled.div({
+ width: '12em',
+ height: '12em',
+ borderRadius: '100%',
+ overflow: 'hidden',
+ border: '#dcdc 1px solid',
+ '& img': {
+ width: '100%',
+ height: '100%',
+ objectFit: 'cover',
+ },
+});
+
+const TopRightWrapper = styled.div({
+ '& div': {
+ margin: '0 4em',
+ '& span': {
+ fontSize: '2em',
+ fontFamily: 'system-ui',
+ },
+ },
+});
+
+export default function UserInfo({ userInfo }) {
+ return (
+
+
+
+
+
+
+
+
+
+ {userInfo.githubId}
+
+
+
+
+ );
+}
diff --git a/front-end/src/UserInfo.test.jsx b/front-end/src/UserInfo.test.jsx
new file mode 100644
index 0000000..2d95b4a
--- /dev/null
+++ b/front-end/src/UserInfo.test.jsx
@@ -0,0 +1,14 @@
+import React from 'react';
+
+import { render } from '@testing-library/react';
+
+import UserInfo from './UserInfo';
+
+import { user } from '../../fixture/data';
+
+test('UserInfo ', () => {
+ const { container, getByAltText } = render();
+
+ expect(getByAltText(`${user.githubId}`)).toHaveAttribute('src', user.githubProfile);
+ expect(container).toHaveTextContent(user.githubId);
+});
diff --git a/front-end/src/UserInfoContainer.jsx b/front-end/src/UserInfoContainer.jsx
new file mode 100644
index 0000000..647edc0
--- /dev/null
+++ b/front-end/src/UserInfoContainer.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+
+import { useSelector } from 'react-redux';
+
+import { get } from './common/utils';
+
+import UserInfo from './UserInfo';
+
+export default function UserInfoContainer() {
+ const devLinkerInfo = useSelector(get('devLinkerInfo'));
+
+ if (!devLinkerInfo) {
+ return
로딩중....
;
+ }
+
+ return (
+
+ );
+}
diff --git a/front-end/src/UserInfoContainer.test.jsx b/front-end/src/UserInfoContainer.test.jsx
new file mode 100644
index 0000000..6e25cd9
--- /dev/null
+++ b/front-end/src/UserInfoContainer.test.jsx
@@ -0,0 +1,45 @@
+import React from 'react';
+
+import { render } from '@testing-library/react';
+
+import { useSelector } from 'react-redux';
+
+import UserInfoContainer from './UserInfoContainer';
+
+import { user } from '../../fixture/data';
+
+jest.mock('react-redux');
+
+describe('', () => {
+ context('with devLinkerInfo', () => {
+ beforeEach(() => {
+ useSelector.mockImplementation((selector) => selector({
+ devLinkerInfo: user,
+ }));
+ });
+
+ it('show devLinkerInfo', () => {
+ const { container, getByAltText } = render(
+ ,
+ );
+
+ expect(getByAltText(`${user.githubId}`)).toHaveAttribute('src', user.githubProfile);
+ expect(container).toHaveTextContent(user.githubId);
+ });
+ });
+
+ context('without devLinkerInfo', () => {
+ beforeEach(() => {
+ useSelector.mockImplementation((selector) => selector({
+ devLinkerInfo: null,
+ }));
+ });
+
+ it('show loading..', () => {
+ const { container } = render(
+ ,
+ );
+ expect(container).toHaveTextContent('로딩중....');
+ });
+ });
+});
diff --git a/front-end/src/UserPage.jsx b/front-end/src/UserPage.jsx
new file mode 100644
index 0000000..e906604
--- /dev/null
+++ b/front-end/src/UserPage.jsx
@@ -0,0 +1,36 @@
+import React, { useEffect } from 'react';
+
+import styled from '@emotion/styled';
+
+import { useParams } from 'react-router-dom';
+
+import { useDispatch } from 'react-redux';
+
+import {
+ loadDevLinkerInfo,
+} from './common/slice';
+
+import UserInfoContainer from './UserInfoContainer';
+
+const Wrapper = styled.div({
+ height: '100vh',
+ padding: '0px 50px',
+ display: 'flex',
+ flexDirection: 'column',
+});
+
+export default function UserPage({ params }) {
+ const dispatch = useDispatch();
+
+ const { devLinkerId } = params || useParams();
+
+ useEffect(() => {
+ dispatch(loadDevLinkerInfo({ devLinkerGithubId: devLinkerId }));
+ }, []);
+
+ return (
+
+
+
+ );
+}
diff --git a/front-end/src/UserPage.test.jsx b/front-end/src/UserPage.test.jsx
new file mode 100644
index 0000000..eaf2373
--- /dev/null
+++ b/front-end/src/UserPage.test.jsx
@@ -0,0 +1,30 @@
+import React from 'react';
+
+import { render } from '@testing-library/react';
+
+import { useDispatch, useSelector } from 'react-redux';
+
+import UserPage from './UserPage';
+
+import { user } from '../../fixture/data';
+
+jest.mock('react-redux');
+jest.mock('../services/firebase/firebase.js');
+
+test('UserPage ', () => {
+ const dispatch = jest.fn();
+
+ useDispatch.mockImplementation(() => dispatch);
+
+ useSelector.mockImplementation((selector) => selector({
+ devLinkerInfo: user,
+ }));
+
+ expect(dispatch).not.toBeCalled();
+
+ render(
+ ,
+ );
+
+ expect(dispatch).toBeCalledTimes(1);
+});
diff --git a/front-end/src/common/actions.test.js b/front-end/src/common/actions.test.js
index 7a4b442..cd50a49 100644
--- a/front-end/src/common/actions.test.js
+++ b/front-end/src/common/actions.test.js
@@ -12,6 +12,8 @@ import {
resetAccessToken,
resetUserInfo,
signUp,
+ loadDevLinkerInfo,
+ setDevLinkerInfo,
} from './slice';
import { user } from '../../../fixture/data';
@@ -92,4 +94,18 @@ describe('actions', () => {
expect(actions[0]).toEqual(setUserInfo(user));
});
});
+
+ describe('loadDevLinkerInfo', () => {
+ beforeEach(() => {
+ store = mockStore({});
+ });
+
+ it('runs setUserInfo', async () => {
+ await store.dispatch(loadDevLinkerInfo({ devLinkerGithubId: user.githubId }));
+
+ const actions = store.getActions();
+
+ expect(actions[0]).toEqual(setDevLinkerInfo(user));
+ });
+ });
});
diff --git a/front-end/src/common/reducer.test.js b/front-end/src/common/reducer.test.js
index 5b6cf4f..412f0d4 100644
--- a/front-end/src/common/reducer.test.js
+++ b/front-end/src/common/reducer.test.js
@@ -4,6 +4,7 @@ import reducer, {
setUserInfo,
resetAccessToken,
resetUserInfo,
+ setDevLinkerInfo,
} from './slice';
import { devLink, user, accessToken } from '../../../fixture/data';
@@ -16,6 +17,7 @@ describe('reducer', () => {
devLinks: [],
accessToken: null,
userInfo: null,
+ devLinkerInfo: null,
};
it('returns initialState', () => {
@@ -93,4 +95,17 @@ describe('reducer', () => {
expect(state.userInfo).toEqual(null);
});
});
+
+ describe('setDevLinkerInfo', () => {
+ it('reset devLinkerInfo', () => {
+ const initialState = {
+ accessToken,
+ devLinkerInfo: null,
+ };
+
+ const state = reducer(initialState, setDevLinkerInfo(user));
+
+ expect(state.devLinkerInfo).toEqual(user);
+ });
+ });
});
diff --git a/front-end/src/common/slice.js b/front-end/src/common/slice.js
index 56922e3..7954f2a 100644
--- a/front-end/src/common/slice.js
+++ b/front-end/src/common/slice.js
@@ -1,6 +1,8 @@
import { createSlice } from '@reduxjs/toolkit';
-import { getDevLinks, getUsers, addUser } from '../../services/api/api';
+import {
+ getDevLinks, getUsers, addUser, getUser,
+} from '../../services/api/api';
import {
githubOAuthLogin,
@@ -19,6 +21,7 @@ const { actions, reducer } = createSlice({
devLinks: [],
accessToken: null,
userInfo: null,
+ devLinkerInfo: null,
},
reducers: {
setDevLinks(state, { payload: devLinks }) {
@@ -51,6 +54,12 @@ const { actions, reducer } = createSlice({
userInfo: null,
};
},
+ setDevLinkerInfo(state, { payload: devLinkerInfo }) {
+ return {
+ ...state,
+ devLinkerInfo,
+ };
+ },
},
});
@@ -60,6 +69,7 @@ export const {
setUserInfo,
resetAccessToken,
resetUserInfo,
+ setDevLinkerInfo,
} = actions;
export function loadInitialData() {
@@ -119,4 +129,12 @@ export const logout = () => async (dispatch) => {
dispatch(resetUserInfo());
};
+export function loadDevLinkerInfo({ devLinkerGithubId }) {
+ return async (dispatch) => {
+ const devLinkerInfo = await getUser({ githubId: devLinkerGithubId });
+
+ dispatch(setDevLinkerInfo(devLinkerInfo));
+ };
+}
+
export default reducer;
diff --git a/front-end/test/user_page_test.js b/front-end/test/user_page_test.js
new file mode 100644
index 0000000..90f8a50
--- /dev/null
+++ b/front-end/test/user_page_test.js
@@ -0,0 +1,15 @@
+const user = {
+ uid: 'Uid1',
+ githubId: 'daadaadaah',
+ githubProfile: 'https://avatars1.githubusercontent.com/u/60481383?s=460&v=4',
+};
+
+Feature('UserPage');
+
+Scenario('사용자 정보가 보인다.', (I) => {
+ I.amOnPage('/user/daadaadaah');
+
+ I.see(user.githubId);
+
+ I.waitForInvisible({ xpath: `//img[@src='${user.githubProfile}']` });
+});