개발 기간

2025-05-17 ~ 2025-05-20 (4일)

 

개발일

2025-05-17

 

Github

https://github.com/SanginJeong/saramin/tree/develop

 

GitHub - SanginJeong/saramin

Contribute to SanginJeong/saramin development by creating an account on GitHub.

github.com

 

 

개발내용

Route설정, Layout (nav), loginPage, registerPage 의 디자인 개발

 

 

1. Route 설정

import { Routes, Route } from 'react-router'
import './App.css'
import AppLayout from './layout/AppLayout'
import HomePage from './pages/homePage/HomePage'
import RegisterPage from './pages/registerPage/RegisterPage'
import LoginPage from './pages/loginPage/LoginPage'

function App() {

  return (
    <>
      <Routes>
        <Route path='/' element={<AppLayout/>}>
          <Route index element={<HomePage/>}/>
          <Route path='/login' element={<LoginPage/>}/>
          <Route path='/register' element={<RegisterPage/>}/>
        </Route>
      </Routes>
    </>
  )
}

export default App

 

일단 모든 페이지가 AppLayout에 포함되어있는 구조로 짰고, AppLayout에서 페이지별로 Outlet을 렌더링 하는 구조로 짰다.

 

추후에 페이지가 추가될 때마다 Route는 추가할 것

 

2. AppLayout (nav)

 

import { Outlet } from 'react-router'
import NavTop from './nav/NavTop'
import NavMenu from './nav/NavMenu'
const AppLayout = () => {
  return (
    <>
      <nav className='shadow-md flex justify-center mb-8'>
        <div className='w-[80%] relative'>
          <NavTop/>
          <NavMenu/>
        </div>
      </nav>
      <div className='flex justify-center'>
        <div className="w-[80%]">
          <Outlet/>
        </div>
      </div>
    </>
  )
}

export default AppLayout

 

페이지를 전체적으로 80% 정도의 너비만 사용해서 페이지의 중앙에 컨텐츠들이 오게끔 하였다.

 

폴더안에 nav폴더를 만들어서 navTop과 navMenu 들로 나눴다.

 

살펴보기전에 최종 결과 사진부터 보고 뜯어보자.

 

기본 Navbar

 

SearchUI 컴포넌트가 열렸을 때

        <div className='w-[80%] relative'>
          <NavTop/>
          <NavMenu/>
          {isOpenSearchUI && <SearchUI/>}
        </div>

 

여기서 SearchUI 컴포넌트의 위치를 처음에는 AppLayout에 아래와 같이 놔뒀었지만 이러면 SearchUI가 열렸을 때 NavMenu 까지 덮어버리게 된다. 내가 하고싶은 것은 SearchUI가 열렸을 때도 NavMenu는 보여줘야하기 때문에 NavTop 안으로 옮겨줬다.

 

 

navTop

import React, { useState } from 'react'
import { Link } from 'react-router'
import { useSearchStore } from '../../store/useSearchStore'
import SearchUI from '../../common/searchUI/SearchUI'

const NavTop = () => {
  const {setSearchUI, isOpenSearchUI} = useSearchStore();
  
  return (
    <div className='flex py-4 relative top-0'>
      <div className='flex gap-4 flex-[8] items-center'>
        <div className='flex-[2]'>
          <Link className='text-[24px] text-blue-800 font-bold' to='/'>Saramin</Link>
        </div>
        <form className='relative flex-[10]' onClick={setSearchUI}>
          <i className="fa-solid fa-magnifying-glass absolute cursor-pointer left-2 top-[15px] text-blue-600"/>
          <input className='input-base px-7 cursor-pointer py-2 w-[60%]' type="text" placeholder='채용 공고 검색'/>
        </form>
      </div>

      <div className='flex-[4] flex items-center gap-4 justify-end'>
        <Link to='login' className='hover:text-blue-600 text-gray-600'>로그인</Link>
        <Link to='register' className='hover:text-blue-600 text-gray-600'>회원가입</Link>
      </div>
      {isOpenSearchUI && <SearchUI/>}
    </div>
  )
}

export default NavTop

 

 

navMenu

import React, { useState } from 'react'

const navMenuList = [
  {title: "menu 1", content: ['menu 1-1', 'menu 1-2', 'menu 1-3', 'menu 1-4']},
  {title: "menu 1", content: ['menu 1-1', 'menu 1-2', 'menu 1-3', 'menu 1-4']},
  {title: "menu 1", content: ['menu 1-1', 'menu 1-2', 'menu 1-3', 'menu 1-4']},
  {title: "menu 1", content: ['menu 1-1', 'menu 1-2', 'menu 1-3', 'menu 1-4']},
  {title: "menu 1", content: ['menu 1-1', 'menu 1-2', 'menu 1-3', 'menu 1-4']},
  {title: "menu 1", content: ['menu 1-1', 'menu 1-2', 'menu 1-3', 'menu 1-4']},
]

const NavMenu = () => {
  const [active, setActive] = useState(null);
  return (
    <div className='py-2 flex gap-4'>
      {navMenuList.map((menu,index)=>(
        <ul className='relative pr-4' onMouseOver={()=>setActive(index)} onMouseLeave={()=>setActive(null)}>
          <p className='hover:bg-blue-600 hover:text-white transition px-4'>{menu.title}</p>
          <ul className={`${index === active ? '' : 'hidden'} absolute -left-3 z-[100] border-1 rounded-md border-gray-400 shadow-md bg-white w-32 transition`}>
            {menu.content.map((subMenu)=>(
              <li className='text-gray-600 px-1 py-2 hover:bg-blue-600 hover:text-white transition'>{subMenu}</li>
            ))}
          </ul>
        </ul>
      ))}
    </div>
  )
}

export default NavMenu

 

드랍다운을 구현했다. 아직 드랍다운의 하위메뉴들을 뭘로 정할지 안정해서 임시 데이터들로 디자인을 구현했다.

 

드랍다운 하위메뉴들의 열리고 닫힘은 useState로 상태관리 해줬다. onMouseOver와 onMouseLeave를 이용해서 index와 active가 일치한다면 열리도록 설정했다.

 

 

 

3. loginPage

import React from 'react'
import { Link } from 'react-router'
const LoginPage = () => {
  return (
    <>
      <div className='flex'>
        <form className='flex flex-1 flex-col gap-4 '>
          <h1 className='self-center text-[32px]'>로그인</h1>
          <input type="text" className='input-base rounded-none p-2' placeholder='아이디'/>
          <input type="password" className='input-base rounded-none p-2' placeholder='비밀번호'/>
          <div className='flex justify-center gap-4 text-gray-400'>
            <Link>아이디 찾기</Link>
            <Link>비밀번호 찾기</Link>
          </div>
        </form>

        <div className='flex-1 flex justify-center items-center'>
          <Link to='/register' className='border-2 py-8 px-12 text-[24px] rounded-2xl bg-blue-600 text-white hover:bg-blue-400 transition'>회원가입</Link>
        </div>
      </div>

      {/* 에러바운더리 */}
    </>
  )
}

export default LoginPage

 

로그인 페이지는 딱히 헷갈렸던 로직이 없고 하던대로 잘 짜져서 스킵.

 

 

4. registerPage

import React from 'react'
import { useNavigate } from 'react-router'

const RegisterPage = () => {
  const navigate = useNavigate();
  return (
    <div className='flex flex-col pb-12'>
      <h1 className='text-[32px] mb-5'>회원가입</h1>
      <form className='flex flex-col gap-4'>
        <div>
          <p>아이디</p>
          <div className='flex gap-4 items-center'>
            <input type="text" className='input-base rounded-none w-[50%] px-2 py-1' />
            {/* 중복체크 시 비교해서 맞으면 체크표시 렌더링 오류있으면 에러문구 띄우기 */}
            <i className="text-green-400 fa-solid fa-check"></i>
            <button type='button' className='button-base bg-blue-600 text-white hover:bg-blue-400 transition'>중복체크</button>
          </div>
        </div>
        <div>
          <p>비밀번호</p>
          <div className='flex gap-4 items-center'>
            <input type="text" className='input-base rounded-none w-[50%] px-2 py-1' placeholder='특수문자, 숫자, 영어를 포함' />
            {/* onChage 시 비교해서 맞으면 체크표시 렌더링 오류있으면 에러문구 띄우기 */}
            <i className="text-green-400 fa-solid fa-check"></i>
          </div>
        </div>
        <div>
          <p>비밀번호 확인</p>
          <div className='flex gap-4 items-center'>
            <input type="text" className='input-base rounded-none w-[50%] px-2 py-1' />
            {/* onChage 시 비교해서 맞으면 체크표시 렌더링 오류있으면 에러문구 띄우기 */}
            <i className="text-green-400 fa-solid fa-check"></i>
          </div>
        </div>
        <div>
          <p>이메일</p>
          <div className='flex gap-4 items-center'>
            <input type="text" className='input-base rounded-none w-[50%] px-2 py-1' />
            <i className="text-green-400 fa-solid fa-check"></i>
          </div>
        </div>
        <div>
          <p>이름 / 나이 / 성별</p>
          <div className='flex gap-4 items-center'>
            <div className='flex gap-4 items-center w-[50%]'>
              <input type="text" className='input-base rounded-none px-2 py-1 flex-1' placeholder='이름'/>
              <select className='border-2 border-blue-600 p-1 flex-1'>
                {Array(42).fill(0).map((v,i)=>(
                  <option value={i+19}>{i+19}</option>
                ))}
              </select>
              <select className='border-2 border-blue-600 p-1 flex-1'>
                <option value="male">남성</option>
                <option value="female">여성</option>
              </select>
            </div>
            <i className="text-green-400 fa-solid fa-check"></i>
          </div>
        </div>

        <div>
          <p>주소</p>
          <div className='input-base rounded-none px-2 py-1 w-[50%] cursor-pointer text-gray-600 mb-1'>주소검색</div>
          <input type="text" className='input-base rounded-none px-2 py-1 w-[50%]' placeholder='상세주소'/>
        </div>
        
        <div className='flex justify-center gap-4'>
          <button type='submit' className='button-base bg-blue-600 text-white hover:bg-blue-400 transition min-w-30'>완료</button>
          <button type='button' onClick={()=>navigate('/login')} className='button-base bg-blue-600 text-white hover:bg-blue-400 transition min-w-30'>돌아가기</button>
        </div>
      </form>
    </div>
  )
}

export default RegisterPage

 

일단 디자인은 다 짜놨다. 체크표시 or 에러메시지가 뜨도록 할 것이다.

지금 딱 보니까 중복되는 코드들 (input 쪽)은 따로 input컴포넌트를 만들어서 props로 받으면 중복코드를 대폭 줄일 수 있을 것 같다. 이건 내일 하려고 한다. 합성컴포넌트 패턴을 사용할까 하다가 이 페이지에서 외에는 다른 용도로는 쓰지 않을 것 같아서 중복만 줄이기에는 적합하지 않을 것 같다. 합성컴포넌트는 모달 재사용 처럼 안에 내용이 많이 바뀌는데 여러곳에서 사용할 때 쓰면 좋다고 생각한다.

 

또, 주소검색은 우편번호 관련 api를 찾아서 모달로 띄울 예정이다.

 <select className='border-2 border-blue-600 p-1 flex-1'>
    {Array(42).fill(0).map((v,i)=>(
      <option value={i+19}>{i+19}</option>
    ))}
</select>

 

option이 42개나 필요해서 임의로 배열을 만들어서 반복문으로 렌더링 해주었다. 알고리즘 문제 연습할 때 배워놨던 Array.fill.map 을 이용했다.

 

 

구현하지 못한 것들

- navbar가 일정 scroll 이상 내려오면 navTop만 고정되도록 하고 싶다. 여기에 시간을 많이써서 오늘 진도를 많이 못나갔다. 내일도 도전한다.

 

 

내일 목표

- 로그인, 회원가입 (주소검색 api 잘 찾는게 중요) 백엔드 구현 및 테스트

- navbar scroll 고정 구현

- searchUI에서 지역선택, 직업선택 클릭시 focus (테두리 찐하게 누가봐도 이게 클릭됐다는게 보일정도)와 하위메뉴 나타나는 것 까지 디자인 (임시 데이터로)

- 모바일 화면 중간점검

1일차 깨우친 점

생각보다 4일안에 하기 정말 어려울 것 같다. 기간을 더 늘려야 할 수도 있다. 아직 api 문서 분석도 못했다... 그래도 navbar에 중요한 기능들 다 해놓으면 50% 라고 생각한다. 나머지 페이지에는 결국 공고들을 카드로 보여주는 식이 대부분 차지하기 때문에 쉬울 것 같다. 배포까지 공부하면.. 4일안에 할 수 있을지 모르겠지만 최대한 내 힘으로 해보자. 그리고 몰랐던 거 잘 체크하자. 내 이번 프로젝트의 목표는 axios, jwt 토큰 저장위치, 배포 3가지를 어느정도 머릿속에 정리하는 것이다. 가보자

 

그리고 css가 시간을 좀 잡아먹는다. tailwind가 아직 미숙한 부분도 있지만 근본적으로 내가 몰랐던 디테일한 것도 많았다.

1. absolute와는 다르게 fixed를 해버리면 부모요소의 크기를 생각하지 않는다.

2. sticky의 조건들 (3가지를 지키지 않으면 sticky가 작동하지 않음)

 

 

문자열이 있다고 치자. 그리고 이 문자열의 각 문자들이 특정 문자와의 거리(가까운 거리)를 구해야 한다.

 

이해하기 쉽게 예시로 teachermode라는 문자열이 있고 특정 문자 e가 입력되면, 출력은

1 0 1 2 1 0 1 2 2 1 0 이다.

 

나는 처음에 문자열의 각 문자를 같은 문자열의 다른 문자와 비교해야하므로 무조건 이중 for문을 써야할 수 밖에 없다고 생각해서 아래와 같이 풀었다.

function solution(s,t){
  const answer = [];
  for(let i=0; i<s.length; i++){
    let min_distance = Number.MAX_SAFE_INTEGER;
    for(let j=0; j<s.length; j++){
      if(s[j] === t){
        const distance = Math.abs(i-j);
        if(distance < min_distance) min_distance = distance;
      }
    }
    answer.push(min_distance);
  }

  return answer.join(" ")
}

 

 

IDEA : 왼쪽에서부터의 거리와 오른쪽에서부터의 거리를 각각 검사하고, 작은것들로 취합.

function solution(s,t) {
  const answer = [];
  let p = 100;
  for(let i=0; i<s.length; i++){
    if(s[i] === t) {
      p = 0;
      answer.push(p);
    } else {
      p++;
      answer.push(p);
    }
  }

  p = 100;
  for(let i=s.length-1; i>=0; i--){
    if(s[i] === t) p=0;
    else {
      p++;
      answer[i] = Math.min(p, answer[i]); // 원래있던 왼쪽에서와의 거리와 현재 오른쪽에서와의 거리중 작은값
    }
  }
  return answer.join(" ")
}

아스키코드

언젠가 쓸 일이 있으니 소문자 a-z 부터 대문자 A-Z까지는 외워두자.

 

대문자 알파벳 : 65~90 까지 26개
소문자 알파벳 : 97~122 까지 26개

 

charCodeAt(), String.fromCharCode()

// 문자의 아스키코드 체크하는 법
'A'.charCodeAt() // 65

// 반대로 아스키코드로 문자 체크하는 법
String.fromCharCode(65) // 'A'

 

replace 로 문자열 바꾸기 + 정규표현식

replace(a,b) -> a를 b로 바꾼다.

 

정규표현식을 이용하면 /A/를 #으로 바꾼다. 다만, A를 만나면 #으로 바꾸고 아예 끝나버리기 때문에 문자열 전체에서 A를 #으로 바꾸려면 g(global)를 붙여준다.

function solution(str) {
  let answer = str;
  str = str.replace('/A/g', '#');
  return answer
}

console.log(solution("BANANA"));

 

정규표현식을 이용하면 문자열에서 알파벳들만 걸러낼 수 있다.

function solution(str) {
  const answer = s.replace("/[^a-z]/g","")
  return answer
}

const s = 'ab ;c, dA'
console.log(solution(s)) // abcd

 

문자열에서 숫자만 걸러내기 + 028 과 같은 문자열 28숫자로 나타내기 원리

 

문자열이 있을 때 숫자만 걸러낼 수 있는 방법

 

isNaN : 문자열이 NaN(숫자가아니면) true 숫자이면 false

function solution(str) {
  let numberStr = "";
  for(let i=0; i<str.length; i++){
    if(!isNaN(str[i])) numberStr+=str[i]
  }
}

const s = 'a0b1 ;c, dA23'

console.log(solution(s)) // "0123"

 

여기서 0123을 123으로 바꾸려면 parseInt, Number, *1 을 해주는 여러가지 내장함수들을 사용했었다. 근데 직접 구현해보자.

어떤 숫자이던 일의자리 숫자 전까지 10을 곱하고 일의자리만 더해주면 그 숫자가 나온다.

즉 0123 이면 10*0+0 = 0, 1*10+2, 12*10+3 이런식으로 표현할 수 있다.

function solution(str) {
  let numberStr = 0;
  for(let i=0; i<str.length; i++){
    if(!isNaN(str[i])) numberStr = numberStr*10 + Number(str[i])
  }
}

const s = 'a0b1 ;c, dA23'

console.log(solution(s)) // "123"

 

알고리즘 문제를 풀다 보면, 최댓값이나 최솟값을 구해야 하는 문제가 종종 있다.

 

나는 지금까지 최솟값을 구할 때나 최댓값을 구할 때 어떤 값으로 초기화 해야할 지 고민하는데 시간을 썼다.

 

쉬운 문제라면 최솟값이나 최댓값의 범위를 알려줘서 (ex. 자연수일 때 최솟값 0으로 초기화) 고민이 없었는데, 어려운 문제를 풀다 보면 설정하기 애매한 조건들이 있었다.

 

이 방법을 왜 이제 알았을까?

 

최소값 초기화 Number.MAX_SAFE_INTEGER

어떤 입력값들을 탐색할 때 최솟값을 가장 크게 해놓는다면, 입력값의 처음 인덱스 혹은 첫 값부터 최솟값이 되고, 이후 모두 탐색할 수 있다.

 

  let min = Number.MAX_SAFE_INTEGER;
  for(let i=0; i<arr.length; i++){
    if(arr[i] < min) {
      min = arr[i];
    }
  }

 

 

최대값 초기화 Number.MIN_SAFE_INTEGER

마찬가지로 최댓값은 반대로 최댓값을 가장 작은 값으로 초기화한다면, 첫 값부터 최댓값이되고, 모두를 탐색할 수 있다.

  let max = Number.MIN_SAFE_INTEGER;
  for(let i=0; i<arr.length; i++){
    if(arr[i] > max) {
      max = arr[i];
    }
  }

white-space 속성을 nowrap으로 사용하게 되면 줄바꿈을 막는다.

white-space : nowrap;

 

 

아래 일부 예시 코드를 보자.

 

.dropdown > li {
  padding : 10px 0px;
  /* white-space: nowrap; */
}

 

 

여기서 white-space: nowrap을 준다면

 

.dropdown > li {
  padding : 10px 0px;
  white-space: nowrap;
}

 

 

import React from "react";
import "./App.css";

export default function App() {
  const handleClick = () => {
    console.log("Button clicked!");
  };

  return (
    <div className="container">
      <button onClick={handleClick}>Clickable Button</button>
      <button style={{ pointerEvents: "none" }}>Disabled Button</button>
    </div>
  );
}

 

마치 removeEventListner 와 비슷하다.

 

사용 예시 ) 만약 어떤 버튼을 클릭했을 때 일어난 UI가 버튼을 막아버려서 다시 사용할 수 없게 조금 가린다면 pointerEvents : none 을 사용해서 UI와 버튼이 겹친 부분을 클릭해도 버튼클릭을 할 수 있다.

client : 뷰포트 내에서 의 x,y 좌표

page : 전체 페이지 에서의 x,y 좌표

 

예시코드

import React from "react";
import './App.css'
export default function App() {
  const onClickBtn = (e) => {
    console.log('client:',e.clientX, e.clientY);
    console.log('page:',e.pageX, e.pageY);
  }

  return (
    <>
      <button 
        style={{height:'300px'}}
        onClick={(e)=>{onClickBtn(e)}}>클릭한 위치</button>
      <div className="page">dd</div>
    </>
  );
}

 

오른쪽 아래 모서리를 클릭했을 때 좌표 차이를 보자.

 

스크롤이 맨위에 있을 때

 

 

스크롤이 아래로 내려 갔을 때

 

 

 

스크롤이 위에있던 아래에있던 정확한 좌표를 원한다면 page를 쓰는게 맞다.

+ Recent posts