├── public
│ ├── data
│ │ └──feeds.json
│ └── index.html
│
├── src
│ ├── components
│ │ └──FeedMake.jsx
│ │ └──Feeds.jsx
│ │ └──GNB.jsx
│ │ └──LoginInputValidator.jsx
│ ├── pages
│ │ └──Home.jsx
│ │ └──Login.jsx
│ │ └──PreAssignmentGuide.jsx
│ ├── styles
│ │ └──colors.js
│ │ └──globalStyles.js
│ └── App.js
│ └── index.js
│
└── README.md
✅ 로그인 컴포넌트를 개발합니다. (최소화 - input 2개, button 1개)
✅ 약간의 랜더링 최적화를 고려해주세요. (Hint: Ref 사용)
✅ 로그인 시(ID, PW 입력 후 버튼 클릭)
✅ useRef dom 조작
const emailRef = useRef();
useEffect(() => {
emailRef.current.focus();
}, []);
하긴했지만,,, 뭔가이상하다....(🔺)
✅ Local Storage 에 로그인 정보 저장 (다시 접속했을 경우에 정보가 유지 되어야 합니다.)
const validate = (e) => {
localStorage.setItem('user', JSON.stringify(e));
setUser(e);
navigate('/home');
};
✅ 메인 페이지로 이동합니다.(로그인이 완료되면)
useEffect(() => {
setLoading(true);
if (user !== null) {
navigate('/home');
}
setLoading(false);
}, [user]);
✅ 로그인 후 이동하는 메인페이지의 GNB를 구현해주세요.
✅ 구현 시 스크롤에 관계 없이 화면 상단에 고정되는 sticky GNB 를 구현해주세요.
const Container = styled.div`
position: sticky;
top: 0;
`
✅ 모바일 사이즈의 경우 가운데 Input 창이 사라져야 하고 양옆으로(space-between) 정렬 되어야 합니다.
const Container = styled.div`
position: sticky;
top: 0;
display: flex;
justify-content: space-between;
@media (max-width: 550px) {
input {
display: none;
}
}
`;
✅ 가장 오른쪽 아이콘을 Logout으로 변경해주세요.
<OneBtn onClick={handleLogout}>logout</OneBtn>
const OneBtn = styled.div`
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
height: 40px;
width: 40px;
border-radius: 20px;
background-color: ${colors.lightGray};
font-size: 12px;
:hover {
background-color: ${colors.darkGray};
}
`;
✅ 그 외 기능은 평가하지 않습니다. (가운데 검색바는 input 요소로만 만들어주세요. 기능은 X)
<input placeholder="검색" />
✅ 이메일과 비밀번호의 유효성을 확인합니다.
✅ 이메일 조건 - @ , . 포함
✅ 비밀번호 조건 - 대문자, 숫자, 특수문자 포함 8자리 이상
const VALIDATOR = {
email: {
required: { value: true, message: '이메일을 입력해 주세요' },
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: '옳바른 이메일형식을 입력해주세요',
},
},
password: {
required: { value: true, message: '비밀번호를 입력해 주세요' },
pattern: {
value: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/i,
message:
'비밀번호는 문자 숫자 특수문자의 조합으로 8자 이상으로 입력해주세요',
},
},
};
✅ 로그인 시 이메일과 비밀번호가 등록되어 있는 것과 일치 여부 확인
Validation 상태를 CSS로 표현해주세요.
<ErrorContainer>
<h2>{formState.errors?.email?.message}</h2>
</ErrorContainer>
const ErrorContainer = styled.div`
height: 10px;
h2 {
color: ${colors.tomato};
}
font-size: 12px;
display: flex;
flex-direction: column;
margin:3px
`;
✅ Email Input Validation Check를 통해 Email 형식이 아닌 경우 표시를해주세요. (ex. boder가 red색상으로 변경)
<LoginInputValidator
register={register}
name={'email'}
isError={formState.errors.email === undefined ? false : true}/>
✅ Password Input Validation Check를 통해 Password 형식이 아닌 경우 표시를 해주세요. (ex. boder가 red색상으로 변경.)
<LoginInputValidator
type="password"
register={register}
name={'password'}
isError={formState.errors.password === undefined ? false : true}/>
✅ Login Button Validation Check가 모두 통과된 경우에만 Button 색상을 진하게 변경해주세요. (통과 되지 못한 경우와 구별이 가능해야 합니다.)
const LoginButton = styled.button`
cursor: ${(p) => (p.valid ? 'pointer' : 'no-drop')};
~~
opacity: ${(p) => (p.valid ? 1 : 0.4)};
`;
✅ 유효성 검사 시 아래 두 가지를 적용해서 구현해주세요.
✅ 정규표현식 사용
email:{
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
}
password:{
value: /^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$/i,
}
✅ Validation 함수 분리
✅ 로그인,로그아웃 시 라우팅 로직을 통해 페이지가 이동 되도록 구현해주세요. (Local Storage)
<Routes>
<Route path="/home" element={<Home user={user} setUser={setUser} />} />
<Route path="login" element={<Login user={user} setUser={setUser} />} />
</Routes>
✅ 로그인이 완료되면 라우터에서 Main Page로 이동되어야 합니다. (history push 사용 X)
const validate = (e) => {
localStorage.setItem('user', JSON.stringify(e));
setUser(e);
navigate('/home');
};
✅ 로그아웃되면 (Local Storage가 삭제되면) Login Page로 이동되어야 합니다.(history push 사용 X)
//GNB.jsx
const handleLogout = () => {
localStorage.removeItem('user');
setUser(null);
};
//Home.jsx
useEffect(() => {
setLoading(true);
if (user === null) {
navigate('/login');
}
setLoading(false);
}, [user]);
✅ 피드 컴포넌트를 개발합니다.
✅ 레이아웃을 인스타그램과 동일하게 구현해주시면 됩니다. (픽셀 단위까지 맞추실 필요는 없으나 보기에 자연스럽도록 개발해주세요.)
✅ 각 Feed의 정보는 public/data 디렉토리에 json형식으로 구성하여 fetch, axios 등을 이용하여 data를 요청해야 합니다.
Feed는 최소 3개이상 랜더링 되도록 구현해주세요.
unsplash random 으로 받아오기!
{
"feeds": [
{
"id":1,
"name": "sseulgirang",
"img": "https://source.unsplash.com/random/500x500",
"comments": [
{
"name": "sseulgirang",
"comment": "하이루"
},
{
"name": "팔로워1",
"comment": "바이루"
}
]
},
{
"id": 2,
"name": "Sseul",
"img": "https://source.unsplash.com/random/500x900",
"comments": [
{
"name": "Sseul",
"comment": "하이루우."
},
{
"name": "팔로워2",
"comment": "바이루우2."
}
]
},
{
"id": 3,
"name": "seulgikim",
"img": "https://source.unsplash.com/random/900x500",
"comments": [
{
"name": "seulgikim",
"comment": "하이루우우."
},
{
"name": "팔로워3",
"comment": "바이루루우우."
}
]
}
]
}
useEffect(() => {
async function init() {
const { data } = await axios.get('/data/feeds.json');
setFeeds(data.feeds);
}
init();
}, []);
🚨 각각의 Feed에 댓글을 추가할 수 있도록 개발해주세요. (Enter key & 클릭으로 게시 가능하도록)
const onEnter = useCallback(
(e) => {
if (e.keyCode === 'Enter') {
addComment(id);
}
},
[feeds, comment]
);
왜 안되지....... 로직은 맞는데.........
✅ Feed는 화면 중앙에 위치 해야하며 모바일 대응이 가능해야 합니다.
✅ 게시 후 Input은 초기화 되어야 합니다.
✅ Feed의 이미지는 자유롭게 사용하시되 각각 사이즈가 각각 달라야 합니다. (ex. 정사각형, 세로가 긴 것, 가로가 긴 것 등)
(사이즈를 변경하셔도 됩니다. 같은 사이즈 X)
✅Feeds의 Image가 로딩된 후 컴포넌트가 로딩 되도록 Loading을 구현해 주세요 (로딩바는 없어도 괜찮습니다. Hint: image.onload)
<img src={img} alt="image" loading="lazy" />
✅ 메인 Page 전체에 반응형 CSS가 적용 되어있는지 평가합니다. (Media Query 사용)
//Feeds.jsx
const Wrap = styled.div`
max-width: 420px;
margin: 0 auto;
`;
//FeedMake.jsx
const ImgBox = styled.div`
max-width: 420px;
height: 420px;
text-align: center;
background-color: #dbdbdb;
img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
`;
//GNB.jsx
@media (max-width: 550px) {
input {
display: none;
}
}