메인페이지 디자인, 사원 관리 api 구현 완료 :push

This commit is contained in:
2024-09-20 16:48:48 +09:00
parent 7dd24ed5e4
commit 7f3a934a18
26 changed files with 1354 additions and 267 deletions

View File

@ -44,7 +44,7 @@ public class WebSecurityConfig {
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(request -> request
.requestMatchers("/", "/api/v1/auth/**", "/api/v1/search/**", "/file/**").permitAll()
.requestMatchers("/", "/api/v1/auth/**", "/api/v1/search/**", "/file/**" , "/api/v1/menu/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/board/**", "/api/v1/user/*").permitAll()
.anyRequest().authenticated()
)

View File

@ -0,0 +1,30 @@
package com.eogns.board_back.controller;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.eogns.board_back.dto.request.auth.SignUpRequestDto;
import com.eogns.board_back.dto.response.auth.SignUpResponseDto;
import com.eogns.board_back.service.AuthService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RestController
@RequestMapping("/api/v1/menu/employee")
@RequiredArgsConstructor
public class EmployeeController{
private final AuthService authService;
@PostMapping("/sign-up")
public ResponseEntity<? super SignUpResponseDto> signUp(
@RequestBody @Valid SignUpRequestDto requestBody
) {
ResponseEntity<? super SignUpResponseDto> response = authService.signUp(requestBody);
return response;
}
}

View File

@ -52,6 +52,23 @@ const SIGN_IN_URL = () => `${API_DOMAIN}/auth/sign-in`;
const SIGN_UP_URL = () => `${API_DOMAIN}/auth/sign-up`;
const EMPLOYEE_SIGN_UP_URL = () => `${API_DOMAIN}/menu/employee/sign-up`;
export const employeeSignUpRequest = async (requestBody: SignUpRequestDto) => {
const result = await axios
.post(EMPLOYEE_SIGN_UP_URL(), requestBody)
.then((response) => {
const responseBody: SignUpResponseDto = response.data;
return responseBody;
})
.catch((error) => {
if (!error.response.data) return null;
const responseBody: ResponseDto = error.response.data;
return responseBody;
});
return result;
};
export const signInRequest = async (requestBody: SignInRequestDto) => {
const result = await axios
.post(SIGN_IN_URL(), requestBody)

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,20 @@
.header-menu {
display: flex;
align-items: center;
padding: 10px 20px;
cursor: pointer;
color: #000; /* 메뉴 텍스트 색상 */
font-weight: bold;
transition: background-color 0.3s, color 0.3s;
}
.header-menu-icon {
width: 20px; /* 이미지 크기 조절 */
height: 20px; /* 이미지 크기 조절 */
margin-right: 8px; /* 텍스트와 이미지 사이의 간격 조절 */
}
.header-menu:hover {
background-color: #f0f0f0; /* 호버 시 배경 색상 */
color: #007bff; /* 호버 시 텍스트 색상 */
}

View File

@ -0,0 +1,13 @@
import React from "react";
import "./menuItem.css"; // 스타일을 포함하는 CSS 파일을 import 합니다.
const MenuItem = ({ text, imageSrc, onClick }) => {
return (
<div className="header-menu" onClick={onClick}>
<img className="header-menu-icon" src={imageSrc} alt={`${text}`} />
{text}
</div>
);
};
export default MenuItem;

View File

@ -1,12 +1,44 @@
import BoardDetail from "views/Board/Detail";
// 메인 페이지 경로를 반환하는 함수
export const MAIN_PATH = () => "/";
// 인증 관련 페이지 경로를 반환하는 함수
export const AUTH_PATH = () => "/auth";
// 검색 결과 페이지 경로를 반환하는 함수
// 검색어를 path 변수로 포함
export const SEARCH_PATH = (searchWord: string) => `/search/${searchWord}`;
// 특정 사용자 페이지 경로를 반환하는 함수
// 사용자 이메일을 path 변수로 포함
export const USER_PATH = (userEmail: string) => `/user/${userEmail}`;
// 게시판 경로를 반환하는 함수
export const BOARD_PATH = () => "/board";
// 특정 게시물 상세 페이지 경로를 반환하는 함수
// 게시물 번호를 path 변수로 포함
export const BOARD_DETAIL_PATH = (boardNumber: string | number) =>
`detail/${boardNumber}`;
// 게시물 작성 페이지 경로를 반환하는 함수
export const BOARD_WRITE_PATH = () => "write";
// 특정 게시물 수정 페이지 경로를 반환하는 함수
// 게시물 번호를 path 변수로 포함
export const BOARD_UPDATE_PATH = (boardNumber: string | number) =>
`update/${boardNumber}`;
// 사원 관리 페이지 경로를 반환하는 함수
export const EMPLOYEE_MANAGEMENT_PATH = () => "/employee/sign-up";
// 권한 관리 페이지 경로를 반환하는 함수
export const PERMISSION_MANAGEMENT_PATH = () => "/menu/permisson";
// 공지사항 페이지 경로를 반환하는 함수
export const NOTICE_PATH = () => "/menu/notice";
/* EMPLOYEE_MANAGEMENT_PATH,
PERMISSION_MANAGEMENT_PATH,
NOTICE_PATH */

View File

@ -10,8 +10,10 @@ import {
MAIN_PATH,
SEARCH_PATH,
USER_PATH,
EMPLOYEE_MANAGEMENT_PATH,
PERMISSION_MANAGEMENT_PATH,
NOTICE_PATH,
} from "constant";
import { keyboardKey } from "@testing-library/user-event";
import { useCookies } from "react-cookie";
import { useBoardStore, useLoginuserStore } from "stores";
import { fileUploadRequest, patchBoardRequest, postBoardRequest } from "apis";
@ -21,83 +23,61 @@ import {
PostBoardResponseDto,
} from "apis/response/board";
import { ResponseDto } from "apis/response";
import MenuItem from "components/MenuItem/menuItem";
// component: 헤더 레이아웃 //
// 헤더 레이아웃
export default function Header() {
// state: cookie 상태 //
// 상태 정의
const [cookies, setCookie] = useCookies();
// state: path 상태 //
const { pathname } = useLocation();
// state: 로그인 유저 상태 //
const { loginUser, setLoginUser, resetLoginUser } = useLoginuserStore();
// state: 로그인 상태 //
const [isLogin, setLogin] = useState<boolean>(false);
// state: 인증 페이지 상태 //
const [isAuthPage, setAuthPage] = useState<boolean>(false);
// state: 메인 페이지 상태 //
const [isMainpage, setMainpage] = useState<boolean>(false);
// state: 검색 상태 //
const [isSearchPage, setSearchPage] = useState<boolean>(false);
// state: 게시물 상세 페이지 상태 //
const [isBoardDetailPage, setBoardDetailPage] = useState<boolean>(false);
// state: 게시물 작성 페이지 상태 //
const [isBoardWritePage, setBoardWritePage] = useState<boolean>(false);
// state: 게시물 수정 페이지 상태 //
const [isBoardUPdatePage, setBoardUPdatePage] = useState<boolean>(false);
// state: 유저 페이지 상태 //
const [isUserPage, setUserPage] = useState<boolean>(false);
// function: 네비게이트 함수 //
const [isEmployeePage, setEmployeePage] = useState<boolean>(false);
const navigate = useNavigate();
// event handler: 로고 클릭 이벤트 처리 함수 //
// 네비게이트 함수
const onLogClickHandler = () => {
navigate(MAIN_PATH());
};
// component: 검색 버튼 컴포넌트 //
// 메뉴 클릭 핸들러
const onMenuClickHandler = (path: string) => {
alert(path);
navigate(path);
};
const onMenuEmployeeClickHandler = () => {
navigate(PERMISSION_MANAGEMENT_PATH());
};
// 검색 버튼 컴포넌트
const SearchButton = () => {
// state: 검색 버튼 요소 참조 상태 //
const searchButtonRef = useRef<HTMLDivElement | null>(null);
// state: 검색 버튼 상태 //
const [status, setStatus] = useState<boolean>(false);
// state: 검색어 상태 //
const [word, setWord] = useState<string>("");
// state: 검색어 path variavble상태 //
const { searchWord } = useParams();
// event Handler: 검색어 변경 이벤트 처리함수 //
const onSearchWordChangeHandler = (
event: ChangeEvent<HTMLInputElement>
) => {
const value = event.target.value;
setWord(value);
setWord(event.target.value);
};
// event Handler: 검색어 키 이벤트 처리함수 //
const onSearchWordkeyDownHandler = (
event: KeyboardEvent<HTMLInputElement>
) => {
if (event.key !== "Enter") return;
if (!searchButtonRef.current) return;
searchButtonRef.current.click();
};
// event Handler: 검색 버튼 클릭 이벤트 처리함수 //
const onSearchButtonClickHandler = () => {
if (!status) {
setStatus(!status);
@ -106,7 +86,6 @@ export default function Header() {
navigate(SEARCH_PATH(word));
};
// effect: 검색어 path varable이 변경 될 때 마다 실행될 함수 //
useEffect(() => {
if (searchWord) {
setWord(searchWord);
@ -114,14 +93,12 @@ export default function Header() {
}
}, [searchWord]);
// render: 검색 버튼 컴포넌트 렌더링(클릭 false) //
if (!status)
return (
<div className="icon-button" onClick={onSearchButtonClickHandler}>
<div className="icon search-light-icon"></div>
</div>
);
// render: 검색 버튼 컴포넌트 렌더링(클릭 true) //
return (
<div className="header-search-input-box">
<input
@ -143,37 +120,32 @@ export default function Header() {
);
};
// component: 마이페이지 버튼 컴포넌트 //
// 마이페이지 버튼 컴포넌트
const MyPageButton = () => {
// state: 유저 이메일 path variable 상태 //
const { userEmail } = useParams();
// event Handler: 마이페이지 버튼 클릭 이벤트 처리 함수 //
const onMyPageButtonClickHandler = () => {
if (!loginUser) return;
const { email } = loginUser;
navigate(USER_PATH(email));
};
// event Handler: 로그아웃 버튼 클릭 이벤트 처리 함수 //
const onSignOutButtonClickHandler = () => {
resetLoginUser();
setCookie("accessToken", "", { path: MAIN_PATH(), expires: new Date() });
navigate(MAIN_PATH());
};
// event Handler: 로그인 버튼 클릭 이벤트 처리 함수 //
const onSignInButtonClickHandler = () => {
navigate(AUTH_PATH());
};
// render: 로그아웃 버튼 컴포넌트 렌더링 //
if (isLogin && userEmail === loginUser?.email)
return (
<div className="white-button" onClick={onSignOutButtonClickHandler}>
{"로그아웃"}
</div>
);
// render: 마이페이지 버튼 컴포넌트 렌더링 //
if (isLogin)
return (
<div className="white-button" onClick={onMyPageButtonClickHandler}>
@ -181,7 +153,6 @@ export default function Header() {
</div>
);
// render: 로그인 버튼 컴포넌트 렌더링 //
return (
<div className="black-button" onClick={onSignInButtonClickHandler}>
{"로그인"}
@ -189,14 +160,11 @@ export default function Header() {
);
};
// component: 업로드 버튼 컴포넌트 //
// 업로드 버튼 컴포넌트
const UploadButton = () => {
// state: 게시물 번호 path variable 상태 //
const { boardNumber } = useParams();
// state: 게시물 상태 //
const { title, content, boardImageFileList, resetBoard } = useBoardStore();
// function: post board response 처리 함수 //
const postBoardResponse = (
responseBody: PostBoardResponseDto | ResponseDto | null
) => {
@ -213,7 +181,6 @@ export default function Header() {
navigate(USER_PATH(email));
};
// function: patch board response 처리 함수 //
const patchBoardResponse = (
responseBody: PatchBoardResponseDto | ResponseDto | null
) => {
@ -228,7 +195,7 @@ export default function Header() {
if (!boardNumber) return;
navigate(BOARD_PATH() + "/" + BOARD_DETAIL_PATH(boardNumber));
};
// event Handler: 업로드 클릭 이벤트 처리 함수 //
const onUploadButtonClickHandler = async () => {
const accessToken = cookies.accessToken;
if (!accessToken) return;
@ -271,47 +238,62 @@ export default function Header() {
postBoardRequest(requestBody, accessToken).then(postBoardResponse);
};
// render: 업로드 버튼 컴포넌트 렌더링 //
if (title && content)
return (
<div className="black-button" onClick={onUploadButtonClickHandler}>
{"업로드1"}
{"업로드"}
</div>
);
// render: 업로드 불가 버튼 컴포넌트 렌더링 //
return <div className="disable-button">{"업로드 불가"}</div>;
};
// effect: path가 변경될 때마다 실행될 함수 //
useEffect(() => {
const isAuthPage = pathname.startsWith(AUTH_PATH());
setAuthPage(isAuthPage);
const isMainpage = pathname === MAIN_PATH();
setMainpage(isMainpage);
const isSearchPage = pathname.startsWith(SEARCH_PATH(""));
setSearchPage(isSearchPage);
const isBoardDetailPage = pathname.startsWith(
BOARD_PATH() + "/" + BOARD_DETAIL_PATH("")
);
setBoardDetailPage(isBoardDetailPage);
const isBoardWritePage = pathname.startsWith(
BOARD_PATH() + "/" + BOARD_WRITE_PATH()
);
setBoardWritePage(isBoardWritePage);
const isBoardUPdatePage = pathname.startsWith(
BOARD_PATH() + "/" + BOARD_UPDATE_PATH("")
);
setBoardUPdatePage(isBoardUPdatePage);
const isUserPage = pathname.startsWith(USER_PATH(""));
setUserPage(isUserPage);
const isEmployeePage = pathname.startsWith(EMPLOYEE_MANAGEMENT_PATH());
setEmployeePage(isEmployeePage);
}, [pathname]);
// effect: 로그인 유저가 변경될 때마다 실행될 함수 //
useEffect(() => {
setLogin(loginUser !== null);
}, [loginUser]);
// render: 헤더 레이아웃 렌더링 //
// state: 서브 메뉴바 상태 //
const [showSubMenu, setShowSubMenu] = useState(null);
const handleMouseEnter = (menu: any) => {
setShowSubMenu(menu);
};
const handleMouseLeave = () => {
setShowSubMenu(null);
};
return (
<div id="header">
<div className="header-container">
@ -321,13 +303,71 @@ export default function Header() {
</div>
<div className="header-logo">{"Layouts Header"}</div>
</div>
{isLogin && (
<div className="header-menu-box">
<div
className="header-menu"
onMouseEnter={() => handleMouseEnter("employee")}
onMouseLeave={handleMouseLeave}
>
<MenuItem
text="사원 관리"
imageSrc="https://cdn-icons-png.flaticon.com/512/159/159833.png"
onClick={() => onMenuEmployeeClickHandler()}
/>
{showSubMenu === "employee" && (
<div className="sub-menu">
<div> </div>
<div> </div>
</div>
)}
</div>
<div
className="header-menu"
onMouseEnter={() => handleMouseEnter("permission")}
onMouseLeave={handleMouseLeave}
>
<MenuItem
text="권한 관리"
imageSrc="https://cdn.iconscout.com/icon/premium/png-256-thumb/access-granted-3192235-2660379.png?f=webp"
onClick={() => onMenuClickHandler(PERMISSION_MANAGEMENT_PATH())}
/>
{showSubMenu === "permission" && (
<div className="sub-menu">
<div> </div>
<div> </div>
</div>
)}
</div>
<div
className="header-menu"
onMouseEnter={() => handleMouseEnter("notice")}
onMouseLeave={handleMouseLeave}
>
<MenuItem
text="공지 사항"
imageSrc="https://png.pngtree.com/png-vector/20190118/ourmid/pngtree-vector-announcement-icon-png-image_323832.jpg"
onClick={() => onMenuClickHandler(NOTICE_PATH())}
/>
{showSubMenu === "notice" && (
<div className="sub-menu">
<div> </div>
<div> </div>
</div>
)}
</div>
</div>
)}
<div className="header-right-box">
{(isAuthPage || isMainpage || isSearchPage || isBoardDetailPage) && (
<SearchButton />
)}
{(isMainpage || isSearchPage || isBoardDetailPage || isUserPage) && (
<MyPageButton />
)}
{(isMainpage ||
isSearchPage ||
isBoardDetailPage ||
isUserPage ||
isEmployeePage) && <MyPageButton />}
{(isBoardWritePage || isBoardUPdatePage) && <UploadButton />}
</div>
</div>

View File

@ -1,8 +1,8 @@
#header {
padding: 20px 120px;
padding: 20px 200px;
display: flex;
justify-items: center;
justify-content: center;
align-items: center;
}
.header-container {
@ -63,3 +63,77 @@
font-weight: 400;
line-height: 140%;
}
.header-menu-box {
margin-left: 50px;
display: flex;
align-items: center;
margin-right: auto; /* to align menus to the left */
}
.header-menu {
position: relative;
padding: 10px 20px;
cursor: pointer;
color: #000; /* menu text color */
font-weight: bold;
transition: background-color 0.3s, color 0.3s;
}
.header-menu:hover {
background-color: #f0f0f0; /* background color on hover */
color: #007bff; /* text color on hover */
}
.sub-menu {
background-color: #ffffff;
border: 1px solid #ccc;
border-radius: 4px;
position: absolute;
top: 100%; /* 부모 메뉴 아래에 위치 */
left: 0;
margin-top: 5px; /* 간격 조정 */
padding: 10px 0;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
display: flex;
flex-direction: column;
}
.sub-menu div {
cursor: pointer;
padding: 8px 16px;
transition: background-color 0.3s;
}
.sub-menu div:hover {
background-color: #f0f0f0;
color: #007bff;
font-weight: bold;
}
.sub-menu div + div {
border-top: 1px solid #e0e0e0;
}
.sub-menu div {
cursor: pointer;
padding: 8px 16px;
transition: background-color 0.3s, color 0.3s; /* 색깔 변화에 대한 트랜지션 추가 */
}
.sub-menu div:hover {
background-color: #f0f0f0;
color: #007bff; /* 호버 시 글자 색깔 변경 */
font-weight: bold;
}
/* 서브 메뉴 항목의 기본 색상 설정 */
.sub-menu div {
color: #333; /* 기본 글자 색 */
}
/* 서브 메뉴 항목 호버 시 색상 변경 */
.sub-menu div:hover {
color: #007bff; /* 원하는 색깔로 변경 */
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@ -134,6 +134,7 @@ export default function Authentication() {
<div className="auth-card">
<div className="auth-card-box">
<div className="auth-card-top">
<div className="auth-card-welcome">{"환영합니다!"}</div>
<div className="auth-card-title-box">
<div className="auth-card-title">{"로그인"}</div>
</div>
@ -763,10 +764,8 @@ export default function Authentication() {
<div className="auth-jumbotron-contents">
<div className="auth-logo-icon"></div>
<div className="auth-jumbotron-text-box">
<div className="auth-jumbotron-text">{"환영합니다"}</div>
<div className="auth-jumbotron-text">
{"홈페이지 만들기 TEST"}
</div>
<div className="auth-jumbotron-text">{""}</div>
<div className="auth-jumbotron-text">{""}</div>
</div>
</div>
</div>

View File

@ -8,10 +8,11 @@
justify-content: center;
align-items: center;
background-image: url(assets/auth-background.png);
background-position: 50% 50%;
background-image: url(assets/t.png);
background-color: rgba(255, 255, 255, 1);
background-position: 15% 25%;
background-repeat: no-repeat;
background-size: auto;
background-size: 60% 60%;
}
.auth-container {
@ -95,6 +96,14 @@
justify-content: space-between;
}
.auth-card-welcome {
color: rgba(0, 0, 0, 1);
font-size: 24px;
font-weight: 300;
line-height: 100%;
}
.auth-card-title {
color: rgba(0, 0, 0, 1);

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 886 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -2,15 +2,15 @@ import React, { useEffect, useState } from "react";
import "./style.css";
import Top3Item from "components/Top3Item";
import { BoardListItem } from "types/interface";
import { latestBoardListMock, top3BoardListMock } from "mocks";
import BoardItem from "components/BoardItem";
import Pagination from "components/Pagination";
import { useNavigate } from "react-router-dom";
import { SEARCH_PATH } from "constant";
import { useNavigate, useParams } from "react-router-dom";
import { MAIN_PATH, SEARCH_PATH } from "constant";
import {
getLatestBoardListRequset,
getPopularListRequest,
getTop3BoardListRequest,
getUserRequest,
} from "apis";
import {
GetLatestBoardListResponseDto,
@ -19,6 +19,11 @@ import {
import { ResponseDto } from "apis/response";
import { usePagination } from "hooks";
import { GetPoplarListResponseDto } from "apis/response/search";
import { useCookies } from "react-cookie";
import { GetUserResponseDto } from "apis/response/user";
import { useLoginuserStore } from "stores";
import DefaultProfileImage from "assets/image/default-profile-image.png";
// component 메인 화면 컴포넌트 //
export default function Main() {
@ -30,6 +35,24 @@ export default function Main() {
// state: 주간 top3 게시물 리스트 상태 //
const [top3BoardList, setTop3BoardList] = useState<BoardListItem[]>([]);
// state: 로그인 상태 //
const [isLogin, setLogin] = useState<boolean>(false);
// state: 로그인 유저 상태 //
const { loginUser } = useLoginuserStore();
// state: 쿠키 상태 //
const [cookies, setCookies] = useCookies();
// state: userEamil path variable 여부 상태 //
const { userEmail } = useParams();
// state: 닉네임 상태 //
const [nickname, setNickname] = useState<string>("");
// state: 프로필 이미지 상태 //
const [profileImage, setProfileImage] = useState<string | null>(null);
// function: getTop3BoardListResponse 처리 함수 //
const getTop3BoardListResponse = (
responseBody: GetTop3BoardListResponseDto | ResponseDto | null
@ -43,23 +66,102 @@ export default function Main() {
setTop3BoardList(top3List);
};
// function: getUserResponse 처리 함수 //
const getUserResponse = (
responseBody: GetUserResponseDto | ResponseDto | null
) => {
if (!responseBody) return;
const { code } = responseBody;
if (code === "NU") alert("존재하지 않는 유저입니다.");
if (code === "DBE") alert("데이터베이스 오류입니다");
if (code !== "SU") {
navigate(MAIN_PATH());
return;
}
const { email, nickname, profileImage } =
responseBody as GetUserResponseDto;
setNickname(nickname);
setProfileImage(profileImage);
/* const isMyPage = email === loginUser?.email;
setMyPage(isMyPage); */
};
// effect: 첫 마운트 시 실행될 함수 //
useEffect(() => {
getTop3BoardListRequest().then(getTop3BoardListResponse);
}, []);
// effect: accessToken cookie 값이 변경될 때 마다 실행할 함수-------------- //
useEffect(() => {
const accessToken = cookies.accessToken;
if (accessToken !== null) {
setLogin(true);
} else {
setLogin(false); // accessToken이 없으면 로그인 상태를 false로 설정
}
}, [cookies]);
// effect: user email path variable 변경시 실행 할 함수 //
useEffect(() => {
if (!loginUser) return; // loginUser가 없으면 함수 실행 중단
const { email } = loginUser; // loginUser에서 email 추출
getUserRequest(email).then(getUserResponse);
console.log(email);
}, [loginUser]); // loginUser가 변경될 때마다 실행
// render 메인화면 상단 컴포넌트 렌더링 //
return (
<div id="main-top-wrapper">
<div className="main-top-container">
<div className="main-top-title">{"Project Demo\n-Main Page-"}</div>
<div className="main-top-contents-box">
<div className="main-top-contents-title">{"주간 TOP3 게시글"}</div>
<div className="main-top-contents">
{top3BoardList.map((top3ListItem) => (
<Top3Item top3ListItem={top3ListItem} />
))}
{loginUser && (
<div className="main-work-wrapper">
<div className="main-top-contents-box1">
<div className="main-top-contents-title">{"주간 게시글"}</div>
<div className="main-top-contents">
{top3BoardList.slice(0, 2).map((top3ListItem) => (
<Top3Item top3ListItem={top3ListItem} />
))}
</div>
</div>
<div className="main-work-container">
<div
className="profile-box"
style={{
backgroundImage: `url(${
profileImage ? profileImage : DefaultProfileImage
})`,
}}
></div>
<div className="welcome">
<span style={{ fontWeight: "bold" }}>{nickname}</span>
.
</div>
<div className="button-box">
<div className="work">{"출근"}</div>
<div className="work-out">{"퇴근"}</div>
<div className="vacation">{"휴가"}</div>
<div className="work-other-place">{"출장"}</div>
</div>
</div>
</div>
</div>
)}
{!loginUser && (
<>
<div className="main-top-title">
{"Project Demo\n-Main Page-"}
</div>
<div className="main-top-contents-box">
<div className="main-top-contents-title">{"주간 게시글"}</div>
<div className="main-top-contents">
{top3BoardList.map((top3ListItem) => (
<Top3Item top3ListItem={top3ListItem} />
))}
</div>
</div>
</>
)}
</div>
</div>
);
@ -121,7 +223,7 @@ export default function Main() {
return (
<div id="main-bottom-wrapper">
<div className="main-bottom-container">
<div className="main-bottom-title">{"최신 게시물"}</div>
<div className="main-bottom-title">{"공지 사항 리스트"}</div>
<div className="main-bottom-contents-box">
<div className="main-bottom-current-contents">
{viewList.map((boardListItem) => (

View File

@ -1,8 +1,14 @@
#main-top-wrapper {
padding: 80px 0 40px;
padding: 40px 0 40px;
display: flex;
justify-content: center;
background-image: url(assets/t5.avif);
background-color: rgba(255, 255, 255, 1);
background-position: 50% 40%;
background-repeat: no-repeat;
background-size: cover;
}
.main-top-container {
@ -14,6 +20,102 @@
align-items: center;
gap: 56px;
}
.main-work-wrapper {
padding: 0 0 45px;
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
}
.main-work-container {
margin-left: -1000px;
width: 350px;
min-width: 350px;
min-height: 150px;
border: none; /* 테두리 */
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
display: flex;
flex-direction: column; /* 세로 배치 */
align-items: center;
gap: 20px;
padding: 20px 0; /* 위 아래 패딩 추가 */
background-color: rgba(255, 255, 255, 1);
}
.profile-box {
border-radius: 50%;
width: 120px;
height: 120px;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(217, 217, 217, 1);
background-position: 50% 50%;
background-size: 100% 100%;
}
.welcome {
font-size: 18px;
font-weight: 200;
text-align: center;
margin-bottom: 10px;
max-width: 200px; /* 최대 너비 200px 설정 */
overflow-wrap: break-word; /* 긴 단어가 줄바꿈되도록 설정 */
}
.button-box {
display: grid;
grid-template-columns: repeat(2, 1fr); /* 2열 */
gap: 10px; /* 버튼들 사이의 간격 */
}
.button-box div {
border-radius: 46px;
width: 95px;
height: 40px;
display: flex;
justify-content: center;
align-items: center;
background-color: rgba(0, 0, 0, 1);
color: rgba(255, 255, 255, 1);
font-size: 14px;
font-weight: 500;
line-height: 140%;
cursor: pointer;
transition: background-color 0.3s ease;
}
.button-box div:hover {
background-color: rgba(0, 0, 0, 0.7);
}
/* 각 버튼의 위치 */
.work {
grid-column: 1;
grid-row: 1;
}
.work-out {
grid-column: 1;
grid-row: 2;
}
.vacation {
grid-column: 2;
grid-row: 1;
}
.work-other-place {
grid-column: 2;
grid-row: 2;
}
.main-top-title {
color: rgba(0, 0, 0, 1);
@ -28,6 +130,14 @@
white-space: pre-wrap; /* 줄바꿈 설정 */
}
.main-top-contents-box1 {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
.main-top-contents-box {
width: 100%;
display: flex;

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,596 @@
import React, {
useState,
KeyboardEvent,
useRef,
ChangeEvent,
useEffect,
} from "react";
import "./style.css";
import InputBox from "components/InputBox";
import { SignInRequestDto, SignUpRequestDto } from "apis/request/auth";
import { signInRequest, signUpRequest, employeeSignUpRequest } from "apis";
import { SignInResponseDto } from "apis/response/auth";
import { ResponseDto } from "apis/response";
import { useCookies } from "react-cookie";
import { MAIN_PATH } from "constant";
import { useNavigate } from "react-router-dom";
import { Address, useDaumPostcodePopup } from "react-daum-postcode";
import SignUpResponseDto from "apis/response/auth/sign-up-response.dto";
export default function employee() {
/* // state: 화면 상태 //
const [view, setView] = useState<"sign-in" | "sign-up">("sign-up"); */
/* // state: 쿠키 상태 //
const [cookies, setCookie] = useCookies(); */
/* // function: 네비게이터 함수 //
const navigate = useNavigate(); */
// component sign up card 컴포넌트 //
const SignUpCard = () => {
// state: 이메일 요소 참조 상태 //
const emailRef = useRef<HTMLInputElement | null>(null);
// state: 패스워드 요소 참조 상태 //
const passwordRef = useRef<HTMLInputElement | null>(null);
// state: 패스워드 확인 요소 참조 상태 //
const passCheckRef = useRef<HTMLInputElement | null>(null);
// state: 닉네임 요소 참조 상태 //
const nicknameRef = useRef<HTMLInputElement | null>(null);
// state: 전화번호 요소 참조 상태 //
const telNumberRef = useRef<HTMLInputElement | null>(null);
// state: 주소 요소 참조 상태 //
const addressRef = useRef<HTMLInputElement | null>(null);
// state: 상세 주소 요소 참조 상태 //
const addressDetailRef = useRef<HTMLInputElement | null>(null);
// state: 페이지 번호 상태 //
const [page, setPage] = useState<1 | 2>(1);
// state: 이메일 상태 //
const [email, setEmail] = useState<string>("");
// state: 패스워드 상태 //
const [password, setPassword] = useState<string>("");
// state: 패스워드 확인 상태 //
const [passCheck, setPassCheck] = useState<string>("");
// state: 닉네임 상태 //
const [nickname, setNickname] = useState<string>("");
// state: 휴대폰 번호 상태 //
const [telNumber, setTelNumber] = useState<string>("");
// state: 주소 상태 //
const [address, setAddress] = useState<string>("");
// state: 상세주소 상태 //
const [addressDetail, setAddressDetail] = useState<string>("");
// state: 개인 정보 동의 타입 상태 //
const [agreedPersonal, setAgreedPersonal] = useState<boolean>(false);
// state: 패스워드 타입 상태 //
const [passwordType, setPasswordType] = useState<"text" | "password">(
"password"
);
// state: 패스워드 확인 타입 상태 //
const [passCheckType, setPassCheckType] = useState<"text" | "password">(
"password"
);
// state: 이메일 에러 상태 //
const [isemailError, setEmailError] = useState<boolean>(false);
// state: 패스워드 에러 상태 //
const [isPasswordError, setPasswordError] = useState<boolean>(false);
// state: 패스워드 확인 에러 상태 //
const [isPassCheckError, setPassCheckError] = useState<boolean>(false);
// state: 이메일 에러 메시지 상태 //
const [emailErrorMessage, setEmailErrorMessage] = useState<string>("");
// state: 패스워드 에러 메시지 상태 //
const [passwordErrorMessage, setPasswordErrorMessage] =
useState<string>("");
// state: 패스워드 확인 에러 메시지 상태 //
const [passCheckErrorMessage, setPassCheckErrorMessage] =
useState<string>("");
// state: 닉네임 에러 상태 //
const [isNicknameError, setNicknameError] = useState<boolean>(false);
// state: 휴대폰 번호 에러 상태 //
const [isTelNumberError, setTelNumberError] = useState<boolean>(false);
// state: 주소 에러 상태 //
const [isAddressError, setAddressError] = useState<boolean>(false);
// state: 개인 정보 동의 에러 상태 //
const [isAgreedPersonalError, setAgreedPersonalError] =
useState<boolean>(false);
// state: 닉네임 에러 메시지 상태 //
const [nicknameErrorMessage, setNicknameErrorMessage] =
useState<string>("");
// state: 휴대폰 에러 메시지 상태 //
const [telNumberErrorMessage, setTelNumberErrorMessage] =
useState<string>("");
// state: 주소 에러 메시지 상태 //
const [addressErrorMessage, setAddressErrorMessage] = useState<string>("");
// state: 패스워드 버튼 아이콘 상태 //
const [passwordButtonIcon, setPasswordButtonIcon] = useState<
"eye-light-off-icon" | "eye-light-on-icon"
>("eye-light-off-icon");
// state: 패스워드 확인 버튼 아이콘 상태 //
const [passCheckButtonIcon, setPassCheckButtonIcon] = useState<
"eye-light-off-icon" | "eye-light-on-icon"
>("eye-light-off-icon");
// function: 다음 주소 검색 팝업 오픈 함수 //
const open = useDaumPostcodePopup();
// function: sign up response 처리 함수 //
const signUpResponse = (
responsBody: SignUpResponseDto | ResponseDto | null
) => {
if (!responsBody) {
alert("네트워크 이상입니다.");
return;
}
const { code } = responsBody;
if (code === "DE") {
setEmailError(true);
setEmailErrorMessage("중복되는 이메일 주소입니다.");
}
if (code === "DN") {
setNicknameError(true);
setNicknameErrorMessage("중복되는 닉네임입니다.");
}
if (code === "DT") {
setTelNumberError(true);
setTelNumberErrorMessage("중복되는 휴대폰 번호입니다.");
}
if (code === "VF") {
alert("모든 값을 입력하세요.");
}
if (code === "DBE") {
alert("데이터 베이스 오류입니다.");
}
if (code !== "SU") return;
};
// event handler: 닉네임 변경 이벤트 처리 //
const onNicknameChangeHandler = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
setNickname(value);
setNicknameError(false);
setNicknameErrorMessage("");
};
// event handler: 휴대폰 번호 변경 이벤트 처리 //
const onTelNumberChangeHandler = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
setTelNumber(value);
setTelNumberError(false);
setTelNumberErrorMessage("");
};
// event handler: 주소 변경 변경 이벤트 처리 //
const onAddressChangeHandler = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
setAddress(value);
setAddressError(false);
setAddressErrorMessage("");
};
// event handler: 상세 주소 변경 이벤트 처리 //
const onAddressDetailChangeHandler = (
event: ChangeEvent<HTMLInputElement>
) => {
const { value } = event.target;
setAddressDetail(value);
};
// event handler: 개인 정보 동의 체크박스 클릭 이벤트 처리 //
const onAgreedPersonalClickHandler = () => {
setAgreedPersonal(!agreedPersonal);
setAgreedPersonalError(false);
};
// event handler: 패스워드 버튼 클릭 이벤트 처리 //
const onPasswordButtonClickHandler = () => {
if (passwordButtonIcon === "eye-light-off-icon") {
setPasswordButtonIcon("eye-light-on-icon");
setPasswordType("text");
} else {
setPasswordButtonIcon("eye-light-off-icon");
setPasswordType("password");
}
};
// event handler: 패스워드 확인 버튼 클릭 이벤트 처리 //
const onPassCheckButtonClickHandler = () => {
if (passCheckButtonIcon === "eye-light-off-icon") {
setPassCheckButtonIcon("eye-light-on-icon");
setPassCheckType("text");
} else {
setPassCheckButtonIcon("eye-light-off-icon");
setPassCheckType("password");
}
};
// event handler: 이메일 변경 이벤트 처리 //
const onEmailChangeHandler = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
setEmail(value);
setEmailError(false);
setEmailErrorMessage("");
};
// event handler: 패스워드 변경 이벤트 처리 //
const onPasswordChangeHandler = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
setPassword(value);
setPasswordError(false);
setPasswordErrorMessage("");
};
// event handler: 패스워드 확인 변경 이벤트 처리 //
const onPassCheckChangeHandler = (event: ChangeEvent<HTMLInputElement>) => {
const { value } = event.target;
setPassCheck(value);
setPassCheckError(false);
setPassCheckErrorMessage("");
};
// event Handler: 다음 버튼 클릭 이벤트 처리 //
const onNextButtonClickHandler = () => {
const emailPattern = /^[a-zA-Z0-9]*@([-.]?[a-zA-Z0-9])*\.[a-zA-Z]{2,4}$/;
const isEmailPattern = emailPattern.test(email);
if (!isEmailPattern) {
setEmailError(true);
setEmailErrorMessage("이메일 주소 포멧이 맞지 않습니다.");
}
const isCheckedPassword = password.trim().length >= 8;
if (!isCheckedPassword) {
setPasswordError(true);
setPasswordErrorMessage("비밀번호는 8자 이상 입력해 주세요.");
}
const isEqualPassword = password === passCheck;
if (!isEqualPassword) {
setPassCheckError(true);
setPassCheckErrorMessage("비밀번호가 일지하지 않습니다.");
}
if (!isEmailPattern || !isCheckedPassword || !isEqualPassword) return;
setPage(2);
};
// event handler: 회원가입 버튼 클릭 이벤트 처리 //
const onSignUpButtonClickHandler = () => {
const emailPattern = /^[a-zA-Z0-9]*@([-.]?[a-zA-Z0-9])*\.[a-zA-Z]{2,4}$/;
const isEmailPattern = emailPattern.test(email);
if (!isEmailPattern) {
setEmailError(true);
setEmailErrorMessage("이메일 주소 포멧이 맞지 않습니다.");
}
const isCheckedPassword = password.length >= 8;
if (!isCheckedPassword) {
setPasswordError(true);
setPasswordErrorMessage("비밀번호는 8자 이상 입력해 주세요.");
}
const isEqualPassword = password === passCheck;
if (!isEqualPassword) {
setPassCheckError(true);
setPassCheckErrorMessage("비밀번호가 일지하지 않습니다.");
}
if (!isEmailPattern || !isCheckedPassword || !isEqualPassword) {
setPage(1);
return;
}
const hasNickname = nickname.length > 0; /* nickname.trim().length 지움*/
if (!hasNickname) {
setNicknameError(true);
setNicknameErrorMessage("닉네임을 입력해주세요.");
}
const telNumberPattern = /^[0-9]{11,13}$/;
const isTelNumberPattern = telNumberPattern.test(telNumber);
if (!isTelNumberPattern) {
setTelNumberError(true);
setTelNumberErrorMessage("숫자만 입력해주세요.");
}
const hasAddress = address.length > 0; /* address.trim().length 지움*/
if (!hasAddress) {
setAddressError(true);
setAddressErrorMessage("주소를 선택해주세요");
}
if (!agreedPersonal) {
setAgreedPersonalError(true);
}
if (!hasNickname || !isTelNumberPattern || !agreedPersonal) return;
const requestBody: SignUpRequestDto = {
email,
password,
nickname,
telNumber,
address,
addressDetail,
agreedPersonal,
};
employeeSignUpRequest(requestBody).then(signUpResponse);
};
// event handler: 로그인 링크 클릭 이벤트 처리 //
const onSignInLinkClickHandler = () => {};
// event handler: 이메일 키 다운 이벤트 처리 //
const onEmailKeyDownHandler = (event: KeyboardEvent<HTMLInputElement>) => {
if (event.key !== "Enter") return;
if (!passwordRef.current) return;
passwordRef.current.focus();
};
// event handler: 패스워드키 다운 이벤트 처리 //
const onPasswordKeyDownHandler = (
event: KeyboardEvent<HTMLInputElement>
) => {
if (event.key !== "Enter") return;
if (!passCheckRef.current) return;
passCheckRef.current.focus();
};
// event handler: 패스워드 확인 키 다운 이벤트 처리 //
const onPassCheckKeyDownHandler = (
event: KeyboardEvent<HTMLInputElement>
) => {
if (event.key !== "Enter") return;
onNextButtonClickHandler();
};
// event handler: 닉네임 키 다운 이벤트 처리 //
const onNicknameKeyDownHandler = (
event: KeyboardEvent<HTMLInputElement>
) => {
if (event.key !== "Enter") return;
if (!telNumberRef.current) return;
telNumberRef.current.focus();
};
// event handler: 폰 번호 키 다운 이벤트 처리 //
const onTelNumberKeyDownHandler = (
event: KeyboardEvent<HTMLInputElement>
) => {
if (event.key !== "Enter") return;
onAddressButtonClickHandler();
};
// event handler: 주소 키 다운 이벤트 처리 //
const onAddressKeyDownHandler = (
event: KeyboardEvent<HTMLInputElement>
) => {
if (event.key !== "Enter") return;
if (!addressDetailRef.current) return;
addressDetailRef.current.focus();
};
// event handler: 상세주소 키 다운 이벤트 처리 //
const onAddressDetailKeyDownHandler = (
event: KeyboardEvent<HTMLInputElement>
) => {
if (event.key !== "Enter") return;
onSignUpButtonClickHandler();
};
// event handler: 다음 주소 검색 완료 이벤트 처리 //
const onComplete = (data: Address) => {
const { address } = data;
setAddress(address);
setAddressError(false);
setAddressErrorMessage("");
if (!addressDetailRef.current) return;
addressDetailRef.current.focus();
};
// event Handler: 주소 버튼 클릭 이벤트 처리 //
const onAddressButtonClickHandler = () => {
open({ onComplete });
};
// effect: 페이지가 변경될 때 마다 실행될 함수 //
useEffect(() => {
if (page == 2) {
if (!nicknameRef.current) return;
nicknameRef.current.focus();
}
}, [page]);
// render: Sign up card 컴포넌트 렌더링 //
return (
<div className="employee-card">
<div className="employee-card-box">
<div className="employee-card-top">
<div className="employee-card-title-box">
<div className="employee-card-title">{"사용자 계정 생성"}</div>
<div className="employee-catd-page">{`${page}/2`}</div>
</div>
{page === 1 && (
<>
<InputBox
ref={emailRef}
label="이메일 주소*"
type="text"
placeholder="이메일 주소를 입력해주세요."
value={email}
onChange={onEmailChangeHandler}
error={isemailError}
message={emailErrorMessage}
onKeyDown={onEmailKeyDownHandler}
/>
<InputBox
ref={passwordRef}
label="비밀번호*"
type={passwordType}
placeholder="비밀번호를 입력해주세요."
value={password}
onChange={onPasswordChangeHandler}
error={isPasswordError}
message={passwordErrorMessage}
icon={passwordButtonIcon}
onButtonClick={onPasswordButtonClickHandler}
onKeyDown={onPasswordKeyDownHandler}
/>
<InputBox
ref={passCheckRef}
label="비밀번호 확인*"
type={passCheckType}
placeholder="비밀번호를 다시 입력해주세요."
value={passCheck}
onChange={onPassCheckChangeHandler}
error={isPassCheckError}
message={passCheckErrorMessage}
icon={passCheckButtonIcon}
onButtonClick={onPassCheckButtonClickHandler}
onKeyDown={onPassCheckKeyDownHandler}
/>
</>
)}
{page === 2 && (
<>
<InputBox
ref={nicknameRef}
label="닉네임*"
type="text"
placeholder="닉네임을 입력해주세요."
value={nickname}
onChange={onNicknameChangeHandler}
error={isNicknameError}
message={nicknameErrorMessage}
onKeyDown={onNicknameKeyDownHandler}
/>
<InputBox
ref={telNumberRef}
label="휴대폰 번호*"
type="text"
placeholder="휴대폰 번호를 입력해주세요."
value={telNumber}
onChange={onTelNumberChangeHandler}
error={isTelNumberError}
message={telNumberErrorMessage}
onKeyDown={onTelNumberKeyDownHandler}
/>
<InputBox
ref={addressRef}
label="주소*"
type="text"
placeholder="주소 찾기"
value={address}
onChange={onAddressChangeHandler}
error={isAddressError}
message={addressErrorMessage}
icon="expand-right-light-icon"
onButtonClick={onAddressButtonClickHandler}
onKeyDown={onAddressKeyDownHandler}
/>
<InputBox
ref={addressDetailRef}
label="상세 주소*"
type="text"
placeholder="상세 주소를 입력해주세요."
value={addressDetail}
onChange={onAddressDetailChangeHandler}
error={false}
onKeyDown={onAddressDetailKeyDownHandler}
/>
</>
)}
</div>
<div className="employee-card-bottom">
{page === 1 && (
<div
className="black-large-full-button"
onClick={onNextButtonClickHandler}
>
{"다음단계"}
</div>
)}
{page === 2 && (
<>
<div className="employee-consent-box">
<div
className="employee-check-box"
onClick={onAgreedPersonalClickHandler}
>
<div
className={`icon ${
agreedPersonal
? "check-round-fill-icon"
: "check-ring-light-icon"
}`}
></div>
</div>
<div
className={
isAgreedPersonalError
? "employee-consent-title-error"
: "employee-consent-title"
}
>
{"개인정보동의"}
</div>
<div className="employee-consent-link">{"더보기 >"}</div>
</div>
<div
className="black-large-full-button"
onClick={onSignUpButtonClickHandler}
>
{"사용자 생성"}
</div>
</>
)}
</div>
</div>
</div>
);
};
// render: 인증화면 컴포넌트 렌더링 //
return (
<div id="employee-wrapper">
<div className="employee-container">
<div className="employee-jumbotron-box">
<div className="employee-jumbotron-contents">
<div className="employee-logo-icon"></div>
<div className="employee-jumbotron-text-box">
<div className="employee-jumbotron-text">{"환영합니다"}</div>
<div className="employee-jumbotron-text">
{"사용자 생성 page"}
</div>
</div>
</div>
</div>
<SignUpCard />
</div>
</div>
);
}

View File

@ -0,0 +1,170 @@
#employee-wrapper {
padding: 100px 0px 150px;
min-width: 1440px;
height: calc(100vh - 68px - 100px - 150px);
min-height: 960px; /* 이거 없애면 카드 짜리몽땅하게 나옴 */
display: flex;
justify-content: center;
align-items: center;
background-image: url(assets/auth-background.png);
background-position: 50% 50%;
background-repeat: no-repeat;
background-size: auto;
}
.employee-container {
width: 1200px;
height: 100%;
display: grid;
grid-template-columns: 7fr 5fr;
column-gap: 78px;
}
.employee-jumbotron-box {
grid-column: 1 / 2;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.employee-jumbotron-contents {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.employee-logo-icon {
width: 50px;
height: 50px;
background-image: url(assets/auth-logo.png);
background-position: 50% 50%;
background-size: 100% 100%;
}
.employee-jumbotron-text-box {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.employee-jumbotron-text {
color: rgba(255, 255, 255, 1);
font-family: "GimhaeGaya";
font-size: 40px;
font-weight: 400;
line-height: 140%;
letter-spacing: -1.2px;
text-align: center;
}
.employee-card {
grid-column: 2 / 3;
border-radius: 10px;
padding: 50px 50px 30px;
background-color: rgba(255, 255, 255, 1);
box-shadow: 0px 4px 47px 0px rgba(0, 0, 0, 0.25);
}
.employee-card-box {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.employee-card-top {
display: flex;
flex-direction: column;
gap: 40px;
}
.employee-card-title-box {
display: flex;
justify-content: space-between;
}
.employee-card-title {
color: rgba(0, 0, 0, 1);
font-size: 24px;
font-weight: 600;
line-height: 140%;
}
.employee-card-page {
color: rgba(0, 0, 0, 0.7);
font-size: 24px;
font-weight: 600;
line-height: 140%;
}
.employee-card-bottom {
display: flex;
flex-direction: column;
gap: 20px;
}
.employee-description-box {
display: flex;
justify-content: center;
}
.employee-description {
color: rgba(0, 0, 0, 0.7);
font-size: 14px;
font-weight: 400;
line-height: 140%;
}
.employee-description-link {
color: rgba(0, 0, 0, 1);
font-weight: 600;
cursor: pointer;
}
.employee-consent-box {
display: flex;
align-items: center;
gap: 6px;
}
.employee-check-box {
cursor: pointer;
width: 24px;
height: 24px;
}
.employee-consent-title {
color: rgba(0, 0, 0, 0.7);
font-size: 14px;
font-weight: 400;
line-height: 140%;
}
.employee-consent-title-error {
color: rgba(255, 0, 0, 0.7);
font-size: 14px;
font-weight: 400;
line-height: 140%;
}
.employee-consent-link {
color: rgba(0, 0, 0, 1);
font-size: 14px;
font-weight: 600;
line-height: 140%;
}

View File

@ -4,8 +4,8 @@
"settings": {
"width": 2000,
"height": 2000,
"scrollTop": -647.182,
"scrollLeft": -201.0012,
"scrollTop": -561.1241,
"scrollLeft": -344.4682,
"zoomLevel": 0.8,
"show": 431,
"database": 4,

View File

@ -4,9 +4,9 @@
"settings": {
"width": 2000,
"height": 2000,
"scrollTop": -450.7084,
"scrollLeft": -220.8091,
"zoomLevel": 0.8,
"scrollTop": -482.7981,
"scrollLeft": -512.5388,
"zoomLevel": 0.7,
"show": 431,
"database": 4,
"databaseName": "",
@ -92,7 +92,7 @@
"color": ""
},
"meta": {
"updateAt": 1726128309470,
"updateAt": 1726716472171,
"createAt": 1725003298900
}
},
@ -179,31 +179,6 @@
"createAt": 1725003329099
}
},
"M44_l38-vVxHyOsq8CRPj": {
"id": "M44_l38-vVxHyOsq8CRPj",
"name": "auth",
"comment": "권한 종류 테이블",
"columnIds": [
"6w-poCexUwKTv11dm80DX",
"SE2wqh_QeOaWncRIlOWvL"
],
"seqColumnIds": [
"6w-poCexUwKTv11dm80DX",
"SE2wqh_QeOaWncRIlOWvL"
],
"ui": {
"x": 664.5354,
"y": 791.1966,
"zIndex": 310,
"widthName": 60,
"widthComment": 93,
"color": ""
},
"meta": {
"updateAt": 1726031760442,
"createAt": 1726031519594
}
},
"y-7FD40j93P014zpeKbvD": {
"id": "y-7FD40j93P014zpeKbvD",
"name": "TB_file",
@ -215,7 +190,8 @@
"0QpiUcDf1av9QLfmjCltW",
"CjgzCf_O-x7EEcNTEkH7W",
"2m0cUujOxYkN1lY9vJdZy",
"Pu329uwVq-ByQGbK3KvJz"
"Pu329uwVq-ByQGbK3KvJz",
"PZ-xcjVURB_3gUVln0zKe"
],
"seqColumnIds": [
"AjF0zhA7k17RPuLP9x7NC",
@ -228,7 +204,8 @@
"HheFQhoJLpiAP99roG36N",
"d94or2DRf2mIUNC3ZLfWV",
"akr_pWs8iohI_f5bRRqzw",
"T9PlNjJKiRkcy_8ItTwyE"
"T9PlNjJKiRkcy_8ItTwyE",
"PZ-xcjVURB_3gUVln0zKe"
],
"ui": {
"x": 849.8114,
@ -239,7 +216,7 @@
"color": ""
},
"meta": {
"updateAt": 1726128306215,
"updateAt": 1726716553141,
"createAt": 1726032771479
}
},
@ -294,15 +271,15 @@
"4EGixbjC44Sw0DlyDgRyc"
],
"ui": {
"x": 1038.2139,
"y": 142.3953,
"x": 1139.4639,
"y": 136.1453,
"zIndex": 746,
"widthName": 60,
"widthComment": 120,
"color": ""
},
"meta": {
"updateAt": 1726128309471,
"updateAt": 1726715523729,
"createAt": 1726125593357
}
},
@ -998,46 +975,6 @@
"createAt": 1726031474345
}
},
"6w-poCexUwKTv11dm80DX": {
"id": "6w-poCexUwKTv11dm80DX",
"tableId": "M44_l38-vVxHyOsq8CRPj",
"name": "auth_user",
"comment": "",
"dataType": "",
"default": "",
"options": 0,
"ui": {
"keys": 0,
"widthName": 60,
"widthComment": 60,
"widthDataType": 60,
"widthDefault": 60
},
"meta": {
"updateAt": 1726031724750,
"createAt": 1726031632128
}
},
"SE2wqh_QeOaWncRIlOWvL": {
"id": "SE2wqh_QeOaWncRIlOWvL",
"tableId": "M44_l38-vVxHyOsq8CRPj",
"name": "auth_admin",
"comment": "",
"dataType": "",
"default": "",
"options": 0,
"ui": {
"keys": 0,
"widthName": 64,
"widthComment": 60,
"widthDataType": 60,
"widthDefault": 60
},
"meta": {
"updateAt": 1726031730704,
"createAt": 1726031632674
}
},
"3zE6e4pkAmYsMkWFXR7Se": {
"id": "3zE6e4pkAmYsMkWFXR7Se",
"tableId": "Qf7oberqyXg9rwB2NXnFB",
@ -1142,19 +1079,19 @@
"id": "0QpiUcDf1av9QLfmjCltW",
"tableId": "y-7FD40j93P014zpeKbvD",
"name": "file_name",
"comment": "파일명",
"comment": "파일명+UID",
"dataType": "VARCHAR2(50)",
"default": "",
"options": 8,
"ui": {
"keys": 0,
"widthName": 60,
"widthComment": 60,
"widthComment": 66,
"widthDataType": 81,
"widthDefault": 60
},
"meta": {
"updateAt": 1726034697376,
"updateAt": 1726716592297,
"createAt": 1726032839926
}
},
@ -1697,6 +1634,26 @@
"updateAt": 1726126986235,
"createAt": 1726126948524
}
},
"PZ-xcjVURB_3gUVln0zKe": {
"id": "PZ-xcjVURB_3gUVln0zKe",
"tableId": "y-7FD40j93P014zpeKbvD",
"name": "originname",
"comment": "파일명",
"dataType": "VARCHAR2(20)",
"default": "",
"options": 8,
"ui": {
"keys": 0,
"widthName": 62,
"widthComment": 60,
"widthDataType": 81,
"widthDefault": 60
},
"meta": {
"updateAt": 1726716614673,
"createAt": 1726716553141
}
}
},
"relationshipEntities": {
@ -1728,34 +1685,6 @@
"createAt": 1726030193552
}
},
"-ARsCEo_NPzq07sSq5MQA": {
"id": "-ARsCEo_NPzq07sSq5MQA",
"identification": false,
"relationshipType": 8,
"startRelationshipType": 2,
"start": {
"tableId": "5jPYxY_W7XcoLCO--IWtf",
"columnIds": [
"0DI-UPqbdhGb3Cl2tFA8O"
],
"x": 513.5517,
"y": 640.1429,
"direction": 2
},
"end": {
"tableId": "Qf7oberqyXg9rwB2NXnFB",
"columnIds": [
"LttbW8cGsGrNKZzpmCS-e"
],
"x": 671.7857,
"y": 624.3135,
"direction": 1
},
"meta": {
"updateAt": 1726030385147,
"createAt": 1726030385147
}
},
"03Bz1xU0ntcL6sONlZ7bS": {
"id": "03Bz1xU0ntcL6sONlZ7bS",
"identification": false,
@ -1804,7 +1733,7 @@
"DlV0i_mi378ktS7TX2MHG"
],
"x": 849.8114,
"y": 818.5025,
"y": 830.5025,
"direction": 1
},
"meta": {
@ -1812,34 +1741,6 @@
"createAt": 1726032969804
}
},
"4l4Tl9-Jkn6-9RofH8Ko2": {
"id": "4l4Tl9-Jkn6-9RofH8Ko2",
"identification": false,
"relationshipType": 16,
"startRelationshipType": 2,
"start": {
"tableId": "5jPYxY_W7XcoLCO--IWtf",
"columnIds": [
"0DI-UPqbdhGb3Cl2tFA8O"
],
"x": 986.8852,
"y": 444.2819,
"direction": 4
},
"end": {
"tableId": "7E1EZCsr9O350-mnaBiSH",
"columnIds": [
"rufc5sONjStlKSZCvgemx"
],
"x": 803.3566000000001,
"y": 281.9072,
"direction": 2
},
"meta": {
"updateAt": 1726125677582,
"createAt": 1726125677582
}
},
"SkzOTC78rK8MhfG26apJu": {
"id": "SkzOTC78rK8MhfG26apJu",
"identification": false,
@ -1887,8 +1788,8 @@
"columnIds": [
"ikgt-Be_4Vn5KxSnjf-y_"
],
"x": 1038.2139,
"y": 230.3953,
"x": 1139.4639,
"y": 224.1453,
"direction": 1
},
"meta": {
@ -2297,12 +2198,6 @@
"comment": 1726030385146
}
],
"-ARsCEo_NPzq07sSq5MQA": [
"relationshipEntities",
1726030385146,
1726031237785,
{}
],
"a97XkxWjV6uRrdF-XOlgO": [
"tableColumnEntities",
1726031243537,
@ -2329,31 +2224,6 @@
"name": 1726031485949
}
],
"M44_l38-vVxHyOsq8CRPj": [
"tableEntities",
1726031519593,
1726031915918,
{
"name": 1726031620188,
"comment": 1726031630427
}
],
"6w-poCexUwKTv11dm80DX": [
"tableColumnEntities",
1726031632127,
-1,
{
"name": 1726031724749
}
],
"SE2wqh_QeOaWncRIlOWvL": [
"tableColumnEntities",
1726031632673,
-1,
{
"name": 1726031730703
}
],
"3zE6e4pkAmYsMkWFXR7Se": [
"tableColumnEntities",
1726031824143,
@ -2418,7 +2288,7 @@
"name": 1726032863465,
"options(notNull)": 1726033018351,
"dataType": 1726033127517,
"comment": 1726034697376
"comment": 1726716592296
}
],
"CjgzCf_O-x7EEcNTEkH7W": [
@ -2537,12 +2407,6 @@
"comment": 1726125677577
}
],
"4l4Tl9-Jkn6-9RofH8Ko2": [
"relationshipEntities",
1726125677577,
1726125687089,
{}
],
"m55Qv1teASnVRvDMAXfc7": [
"tableColumnEntities",
1726125691894,
@ -2750,6 +2614,17 @@
"dataType": 1726126978821,
"comment": 1726126986235
}
],
"PZ-xcjVURB_3gUVln0zKe": [
"tableColumnEntities",
1726716553139,
-1,
{
"name": 1726716559843,
"dataType": 1726716603538,
"comment": 1726716612601,
"options(notNull)": 1726716614673
}
]
}
}