근태관리 api(미완), 공지사항 파트 구현완료 :push

This commit is contained in:
2024-09-23 17:13:14 +09:00
parent 6da52b67c1
commit dea13c5361
21 changed files with 891 additions and 154 deletions

View File

@ -45,7 +45,7 @@ public class WebSecurityConfig {
)
.authorizeHttpRequests(request -> request
.requestMatchers("/", "/api/v1/auth/**", "/api/v1/search/**", "/file/**" , "/api/v1/menu/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/board/**", "/api/v1/user/*").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/board/**", "/api/v1/user/**").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(exceptionHandling -> exceptionHandling
@ -82,90 +82,3 @@ class FailedAuthenticationEntryPoint implements AuthenticationEntryPoint {
}
/* package com.eogns.board_back.config;
import java.io.IOException;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import com.eogns.board_back.filter.jwtAuthenticationFilter;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
@SuppressWarnings("rawtypes") // 이거 빠른수정으로 추가
private final jwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
protected SecurityFilterChain configure(HttpSecurity httpSecurity) throws Exception{
httpSecurity
.cors(cors -> cors
.configurationSource(corsConfigurationSource())
)
.csrf(CsrfConfigurer::disable)
.httpBasic(HttpBasicConfigurer::disable)
.sessionManagement(sessionManagement -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(request -> request
.requestMatchers("/","/api/v1/auth/**", "api/v1/search/**","/file/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/board/**", "/api/v1/user/*").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(exceptionHandling -> exceptionHandling
.authenticationEntryPoint(new FailedAuthenticationEntryPoint())
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
httpSecurity.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
@Bean
protected CorsConfigurationSource corsConfigurationSource(){
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("*");
configuration.addAllowedMethod("*");
configuration.addExposedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
class FailedAuthenticationEntryPoint implements AuthenticationEntryPoint{
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"code:\": \"AF\", \"message\": \"Authorizion Filed. \"}");
}
}
*/

View File

@ -0,0 +1,34 @@
package com.eogns.board_back.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.eogns.board_back.entity.AttendanceEntity;
import com.eogns.board_back.service.AttendanceService;
import java.util.List;
@RestController
@RequestMapping("/api/v1/user")
public class AttendanceController {
@Autowired
private AttendanceService attendanceService;
@PostMapping("/check-in/{email}")
public AttendanceEntity checkIn(@PathVariable String email) {
return attendanceService.checkIn(email);
}
@PostMapping("/check-out/{email}")
public AttendanceEntity checkOut(@PathVariable String email) {
return attendanceService.checkOut(email);
}
@GetMapping("/test/{email}")
public List<AttendanceEntity> getAttendance(@PathVariable String email) {
return attendanceService.getAttendance(email);
}
}

View File

@ -0,0 +1,33 @@
package com.eogns.board_back.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.eogns.board_back.entity.LeaveRecordsEntity;
import com.eogns.board_back.service.LeaveRecordsService;
import java.time.LocalDate;
import java.util.List;
@RestController
@RequestMapping("/api/v1/user")
public class LeaveRecordsController {
@Autowired
private LeaveRecordsService leaveRecordsService;
@PostMapping("/leave/apply")
public LeaveRecordsEntity applyLeave(
@RequestParam String email,
@RequestParam String status,
@RequestParam LocalDate startDate,
@RequestParam LocalDate endDate,
@RequestParam(required = false) String location) {
return leaveRecordsService.applyLeave(email, status, startDate, endDate, location);
}
@GetMapping("/leave/{email}")
public List<LeaveRecordsEntity> getLeaveRecords(@PathVariable String email) {
return leaveRecordsService.getLeaveRecords(email);
}
}

View File

@ -14,17 +14,19 @@ import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
@RestController
@RequestMapping("/api/v1/menu/employee")
@RequestMapping("/api/v1/menu")
@RequiredArgsConstructor
public class EmployeeController{
public class MenuController{
private final AuthService authService;
@PostMapping("/sign-up")
@PostMapping("/employee/sign-up")
public ResponseEntity<? super SignUpResponseDto> signUp(
@RequestBody @Valid SignUpRequestDto requestBody
) {
ResponseEntity<? super SignUpResponseDto> response = authService.signUp(requestBody);
return response;
}
}

View File

@ -0,0 +1,62 @@
package com.eogns.board_back.entity;
import java.time.LocalDateTime;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class AttendanceEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private LocalDateTime checkInTime;
private LocalDateTime checkOutTime;
private LocalDateTime recordedAt;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public LocalDateTime getCheckInTime() {
return checkInTime;
}
public void setCheckInTime(LocalDateTime checkInTime) {
this.checkInTime = checkInTime;
}
public LocalDateTime getCheckOutTime() {
return checkOutTime;
}
public void setCheckOutTime(LocalDateTime checkOutTime) {
this.checkOutTime = checkOutTime;
}
public LocalDateTime getRecordedAt() {
return recordedAt;
}
public void setRecordedAt(LocalDateTime recordedAt) {
this.recordedAt = recordedAt;
}
}

View File

@ -0,0 +1,80 @@
package com.eogns.board_back.entity;
import java.time.LocalDate;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class LeaveRecordsEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String email;
private String status; // 'vacation' or 'business trip'
private LocalDate vacationStartDate;
private LocalDate vacationEndDate;
private String businessTripLocation;
private LocalDate recordedAt;
// Getters and Setters
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public LocalDate getVacationStartDate() {
return vacationStartDate;
}
public void setVacationStartDate(LocalDate vacationStartDate) {
this.vacationStartDate = vacationStartDate;
}
public LocalDate getVacationEndDate() {
return vacationEndDate;
}
public void setVacationEndDate(LocalDate vacationEndDate) {
this.vacationEndDate = vacationEndDate;
}
public String getBusinessTripLocation() {
return businessTripLocation;
}
public void setBusinessTripLocation(String businessTripLocation) {
this.businessTripLocation = businessTripLocation;
}
public LocalDate getRecordedAt() {
return recordedAt;
}
public void setRecordedAt(LocalDate recordedAt) {
this.recordedAt = recordedAt;
}
}

View File

@ -0,0 +1,15 @@
package com.eogns.board_back.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.eogns.board_back.entity.AttendanceEntity;
import java.util.List;
public interface AttendanceRepository extends JpaRepository<AttendanceEntity, Long>{
// 출근 시간 기록이 없고, 주어진 이메일의 첫 번째 AttendanceEntity를 찾기
AttendanceEntity findFirstByEmailAndCheckOutTimeIsNull(String email);
// 주어진 이메일로 모든 AttendanceEntity 리스트 찾기
List<AttendanceEntity> findByEmail(String email);
}

View File

@ -0,0 +1,13 @@
package com.eogns.board_back.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.eogns.board_back.entity.LeaveRecordsEntity;
import java.util.List;
public interface LeaveRecordsRepository extends JpaRepository<LeaveRecordsEntity, Long> {
// 추가적인 쿼리 메소드 정의 가능
List<LeaveRecordsEntity> findByEmail(String email);
}

View File

@ -0,0 +1,37 @@
package com.eogns.board_back.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.eogns.board_back.entity.AttendanceEntity;
import com.eogns.board_back.repository.AttendanceRepository;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class AttendanceService {
@Autowired
private AttendanceRepository attendanceRepository;
public AttendanceEntity checkIn(String email) {
AttendanceEntity attendance = new AttendanceEntity();
attendance.setEmail(email);
attendance.setCheckInTime(LocalDateTime.now());
attendance.setRecordedAt(LocalDateTime.now());
return attendanceRepository.save(attendance);
}
public AttendanceEntity checkOut(String email) {
AttendanceEntity attendance = attendanceRepository.findFirstByEmailAndCheckOutTimeIsNull(email);
if (attendance != null) {
attendance.setCheckOutTime(LocalDateTime.now());
return attendanceRepository.save(attendance);
}
return null; // 또는 예외 처리
}
public List<AttendanceEntity> getAttendance(String email) {
return attendanceRepository.findByEmail(email);
}
}

View File

@ -0,0 +1,32 @@
package com.eogns.board_back.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.eogns.board_back.entity.LeaveRecordsEntity;
import com.eogns.board_back.repository.LeaveRecordsRepository;
import java.util.List;
import java.time.LocalDate;
@Service
public class LeaveRecordsService {
@Autowired
private LeaveRecordsRepository leaveRecordsRepository;
public LeaveRecordsEntity applyLeave(String email, String status, LocalDate startDate, LocalDate endDate, String location) {
LeaveRecordsEntity leaveRecords = new LeaveRecordsEntity();
leaveRecords.setEmail(email);
leaveRecords.setStatus(status);
leaveRecords.setVacationStartDate(startDate);
leaveRecords.setVacationEndDate(endDate);
leaveRecords.setBusinessTripLocation(location);
leaveRecords.setRecordedAt(LocalDate.now());
return leaveRecordsRepository.save(leaveRecords);
}
public List<LeaveRecordsEntity> getLeaveRecords(String email) {
return leaveRecordsRepository.findByEmail(email);
}
}

View File

@ -8,7 +8,13 @@ import BoardDetail from "views/Board/Detail";
import BoardWrite from "views/Board/Write";
import BoardUptdade from "views/Board/Update";
import Container from "layouts/Container";
import { EMPLOYEE_MANAGEMENT_PATH, MAIN_PATH } from "constant";
import NoticeList from "views/Menu/notice/list";
import {
EMPLOYEE_MANAGEMENT_PATH,
MAIN_PATH,
NOTICE_LIST_PATH,
NOTICE_WRITE_PATH,
} from "constant";
import { AUTH_PATH } from "constant";
import { SEARCH_PATH } from "constant";
import { USER_PATH } from "constant";
@ -24,6 +30,7 @@ import { GetSignInUserResponseDto } from "apis/response/user";
import { ResponseDto } from "apis/response";
import User from "types/interface/user.interface";
import Employee from "views/Menu/employee";
import NoticeWrite from "views/Menu/notice/write";
// component: 어플리케이션 컴포넌트 //
function App() {
@ -64,7 +71,8 @@ function App() {
// discription: 게시물 상세보기 : '/board/detail/:boardNumber' - BoardDetail //
// discription: 게시물 작성하기 : '/board/write' - BoardWrite //
// discription: 게시물 수정하기 : '/board/update/:boardNumber' - BoardUpdate //
// discription: 사원 추가 : '/employee/sign-in' - BoardUpdate //
// discription: 사원 추가 : '/employee/sign-in' - Employee //
// discription: 공지 목록 : '/motice/list' - Notice //
return (
<Routes>
@ -74,6 +82,9 @@ function App() {
<Route path={SEARCH_PATH(":searchWord")} element={<Search />} />
<Route path={USER_PATH(":userEmail")} element={<UserP />} />
<Route path={EMPLOYEE_MANAGEMENT_PATH()} element={<Employee />} />
<Route path={NOTICE_LIST_PATH()} element={<NoticeList />} />
<Route path={NOTICE_WRITE_PATH()} element={<NoticeWrite />} />
<Route path={BOARD_PATH()}>
<Route path={BOARD_WRITE_PATH()} element={<BoardWrite />} />
<Route

View File

@ -36,8 +36,11 @@ export const EMPLOYEE_MANAGEMENT_PATH = () => "/employee/sign-up";
// 권한 관리 페이지 경로를 반환하는 함수
export const PERMISSION_MANAGEMENT_PATH = () => "/menu/permisson";
// 공지사항 페이지 경로를 반환하는 함수
export const NOTICE_PATH = () => "/menu/notice";
// 공지사항 목록 페이지 경로를 반환하는 함수
export const NOTICE_LIST_PATH = () => "/notice/list";
// 공지사항 작성 페이지 경로를 반환하는 함수
export const NOTICE_WRITE_PATH = () => "/notice/write";
/* EMPLOYEE_MANAGEMENT_PATH,
PERMISSION_MANAGEMENT_PATH,

View File

@ -12,7 +12,8 @@ import {
USER_PATH,
EMPLOYEE_MANAGEMENT_PATH,
PERMISSION_MANAGEMENT_PATH,
NOTICE_PATH,
NOTICE_LIST_PATH,
NOTICE_WRITE_PATH,
} from "constant";
import { useCookies } from "react-cookie";
import { useBoardStore, useLoginuserStore } from "stores";
@ -25,9 +26,9 @@ import {
import { ResponseDto } from "apis/response";
import MenuItem from "components/MenuItem/menuItem";
// 헤더 레이아웃
// state: 헤더 레이아웃
export default function Header() {
// 상태 정의
// state: 상태 정의
const [cookies, setCookie] = useCookies();
const { pathname } = useLocation();
const { loginUser, setLoginUser, resetLoginUser } = useLoginuserStore();
@ -37,26 +38,32 @@ export default function Header() {
const [isSearchPage, setSearchPage] = useState<boolean>(false);
const [isBoardDetailPage, setBoardDetailPage] = useState<boolean>(false);
const [isBoardWritePage, setBoardWritePage] = useState<boolean>(false);
const [isNoticeWritePage, setNoticeWritePage] = useState<boolean>(false);
const [isBoardUPdatePage, setBoardUPdatePage] = useState<boolean>(false);
const [isUserPage, setUserPage] = useState<boolean>(false);
const [isEmployeePage, setEmployeePage] = useState<boolean>(false);
const navigate = useNavigate();
// 네비게이트 함수
// function: 네비게이트 함수 //
const onLogClickHandler = () => {
navigate(MAIN_PATH());
};
// 메뉴 클릭 핸들러
// event handler: 메뉴 클릭 핸들러 //
const onMenuClickHandler = (path: string) => {
navigate(path);
};
// event handler: 사이드 카드 클릭 이벤트 처리 //
const onSideCardClickHandler = () => {
navigate(NOTICE_WRITE_PATH());
};
const onMenuEmployeeClickHandler = () => {
navigate(EMPLOYEE_MANAGEMENT_PATH());
};
// 검색 버튼 컴포넌트
// component: 검색 버튼 컴포넌트 //
const SearchButton = () => {
const searchButtonRef = useRef<HTMLDivElement | null>(null);
const [status, setStatus] = useState<boolean>(false);
@ -119,7 +126,7 @@ export default function Header() {
);
};
// 마이페이지 버튼 컴포넌트
// component: 마이페이지 버튼 컴포넌트 //
const MyPageButton = () => {
const { userEmail } = useParams();
@ -159,7 +166,7 @@ export default function Header() {
);
};
// 업로드 버튼 컴포넌트
// component: 업로드 버튼 컴포넌트 //
const UploadButton = () => {
const { boardNumber } = useParams();
const { title, content, boardImageFileList, resetBoard } = useBoardStore();
@ -192,7 +199,13 @@ export default function Header() {
if (code !== "SU") return;
if (!boardNumber) return;
navigate(BOARD_PATH() + "/" + BOARD_DETAIL_PATH(boardNumber));
if (isBoardWritePage) {
navigate(BOARD_PATH() + "/" + BOARD_DETAIL_PATH(boardNumber));
alert("작성 완료되었습니다.");
} else if (isNoticeWritePage) {
MAIN_PATH();
alert("작성 완료되었습니다.");
}
};
const onUploadButtonClickHandler = async () => {
@ -210,7 +223,8 @@ export default function Header() {
}
const isWriterPage = pathname === BOARD_PATH() + "/" + BOARD_WRITE_PATH();
if (isWriterPage) {
const isNoticeWritePage = pathname === NOTICE_WRITE_PATH();
if (isWriterPage || isNoticeWritePage) {
const requestBody: PostBoardRequestDto = {
title,
content,
@ -266,6 +280,9 @@ export default function Header() {
);
setBoardWritePage(isBoardWritePage);
const isNoticeWritePage = pathname.startsWith(NOTICE_WRITE_PATH());
setNoticeWritePage(isNoticeWritePage);
const isBoardUPdatePage = pathname.startsWith(
BOARD_PATH() + "/" + BOARD_UPDATE_PATH("")
);
@ -276,8 +293,6 @@ export default function Header() {
const isEmployeePage = pathname.startsWith(EMPLOYEE_MANAGEMENT_PATH());
setEmployeePage(isEmployeePage);
if (isEmployeePage) alert("삑");
}, [pathname]);
useEffect(() => {
@ -294,7 +309,7 @@ export default function Header() {
const handleMouseLeave = () => {
setShowSubMenu(null);
};
// render: 헤더 렌더링 //
return (
<div id="header">
<div className="header-container">
@ -350,12 +365,16 @@ export default function Header() {
<MenuItem
text="공지 사항"
imageSrc="https://png.pngtree.com/png-vector/20190118/ourmid/pngtree-vector-announcement-icon-png-image_323832.jpg"
onClick={() => onMenuClickHandler(NOTICE_PATH())}
onClick={() => onMenuClickHandler(NOTICE_LIST_PATH())}
/>
{showSubMenu === "notice" && (
<div className="sub-menu">
<div> </div>
<div> </div>
<div onClick={() => onMenuClickHandler(NOTICE_WRITE_PATH())}>
</div>
<div onClick={() => onMenuClickHandler(NOTICE_LIST_PATH())}>
</div>
</div>
)}
</div>
@ -371,7 +390,9 @@ export default function Header() {
isBoardDetailPage ||
isUserPage ||
isEmployeePage) && <MyPageButton />}
{(isBoardWritePage || isBoardUPdatePage) && <UploadButton />}
{(isBoardWritePage || isBoardUPdatePage || isNoticeWritePage) && (
<UploadButton />
)}
</div>
</div>
</div>

View File

@ -10,3 +10,15 @@ export default interface BoardListItem {
writerNickname: string;
writerProfileImage: string | null;
}
export default interface BoardListItemNoProfileImage {
boardNumber: number;
title: string;
content: string;
boardTitleImage: string | null;
favoriteCount: number;
commentCount: number;
viewCount: number;
writeDatetime: string;
writerNickname: string;
}

View File

@ -1,6 +1,14 @@
import User from "views/User";
import BoardListItem from "./board-list-item.interface";
import BoardListItemNoProfileImage from "./board-list-item.interface";
import FavoriteListItem from "./favorite-list-item.interface";
import CommentListItem from "./comment-list-item.interface";
import Board from "./board.interface";
export type { Board, User, BoardListItem, FavoriteListItem, CommentListItem };
export type {
Board,
User,
BoardListItem,
FavoriteListItem,
CommentListItem,
BoardListItemNoProfileImage,
};

View File

@ -178,7 +178,7 @@ export default function Main() {
viewPageList,
totalSection /* 전체 섹션이 몇개인지 */,
setTotalList,
} = usePagination<BoardListItem>(5); /* 5개씩 게시물 보여짐 */
} = usePagination<BoardListItem>(3); /* 5개씩 게시물 보여짐 */
// state: 인기 검색어 리스트 상태 //
const [popularWordList, setPopularWordList] = useState<string[]>([]);
@ -223,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

@ -0,0 +1,137 @@
import React, { useEffect, useState } from "react";
import "./style.css";
import Top3Item from "components/Top3Item";
import { BoardListItem } from "types/interface";
import BoardItem from "components/BoardItem";
import Pagination from "components/Pagination";
import { useNavigate, useParams } from "react-router-dom";
import { MAIN_PATH, SEARCH_PATH } from "constant";
import {
getLatestBoardListRequset,
getPopularListRequest,
getTop3BoardListRequest,
getUserRequest,
} from "apis";
import {
GetLatestBoardListResponseDto,
GetTop3BoardListResponseDto,
} from "apis/response/board";
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 NoticeList() {
// function: 네비게이트 함수 //
const navigate = useNavigate();
// component 메인 화면 하단 컴포넌트 //
const NoticeList = () => {
// state: 페이지네이션 관련 상태 //
const {
currentPage /* 현재 페이지가 어떤 위치에 있는지 */,
setCurrentPage,
currentSection,
setCurrentSection,
viewList /* 현재 보여줄 리스트 */,
viewPageList,
totalSection /* 전체 섹션이 몇개인지 */,
setTotalList,
} = usePagination<BoardListItem>(5); /* 5개씩 게시물 보여짐 */
// state: 인기 검색어 리스트 상태 //
const [popularWordList, setPopularWordList] = useState<string[]>([]);
// function: getLatestBoardListResponse 처리함수 //
const getLatestBoardListResponse = (
responseBody: GetLatestBoardListResponseDto | ResponseDto | null
) => {
if (!responseBody) return;
const { code } = responseBody;
if (code === "DBE") alert("데이터베이스 오류입니다.");
if (code !== "SU") return;
const { latestList } = responseBody as GetLatestBoardListResponseDto;
setTotalList(latestList);
};
// function: getPopularListResponse 처리함수 //
const getPopularListResponse = (
responseBody: GetPoplarListResponseDto | ResponseDto | null
) => {
if (!responseBody) return;
const { code } = responseBody;
if (code === "DBE") alert("데이터베이스 오류입니다.");
if (code !== "SU") return;
const { popularWordList } = responseBody as GetPoplarListResponseDto;
setPopularWordList(popularWordList);
};
// event handler: 인기 검색어 클릭 이벤트 처리 //
const onPopularWordClickHandler = (word: string) => {
navigate(SEARCH_PATH(word));
};
// effect: 첫 마운트 시 실행될 함수 //
useEffect(() => {
getLatestBoardListRequset().then(getLatestBoardListResponse);
getPopularListRequest().then(getPopularListResponse);
}, []);
// render: 메인 화면 하단 컴포넌트 렌더링 //
return (
<div id="notice-bottom-wrapper">
<div className="notice-bottom-container">
<div className="notice-bottom-title">{"공지 사항 리스트"}</div>
<div className="notice-bottom-contents-box">
<div className="notice-bottom-current-contents">
{viewList.map((boardListItem) => (
<BoardItem boardListItem={boardListItem} />
))}
</div>
<div className="notice-bottom-popular-box">
<div className="notice-bottom-popular-card">
<div className="notice-bottom-popular-card-box">
<div className="notice-bottom-popular-card-title">
{"인기 검색어"}
</div>
<div className="notice-bottom-popular-card-contents">
{popularWordList.map((word) => (
<div
className="word-badge"
onClick={() => onPopularWordClickHandler(word)}
>
{word}
</div>
))}
</div>
</div>
</div>
</div>
</div>
<div className="notice-bottom-pagination-box">
<Pagination
currentPage={currentPage}
currentSection={currentSection}
setCurrentPage={setCurrentPage}
setCurrentSection={setCurrentSection}
viewPageList={viewPageList}
totalSection={totalSection}
/>
</div>
</div>
</div>
);
};
// render 메인화면 컴포넌트 렌더링 //
return (
<>
<NoticeList />
</>
);
}

View File

@ -0,0 +1,79 @@
#notice-bottom-wrapper {
padding: 40px 0;
display: flex;
justify-content: center;
background-color: rgba(0, 0, 0, 0.05);
}
.notice-bottom-container {
width: 1200px;
min-width: 1200px;
display: flex;
flex-direction: column;
gap: 20px;
}
.notice-bottom-title {
color: rgba(0, 0, 0, 1);
font-size: 24px;
font-weight: 500;
line-height: 140%;
}
.notice-bottom-contents-box {
width: 100%;
display: grid;
grid-template-columns: 8fr 4fr; /* 최신 게시물과 인기검색어 간의 비율 2 : 1 비율로 설정함 */
gap: 24px;
}
.notice-bottom-current-contents {
grid-column: 1 / 2;
display: flex;
flex-direction: column;
gap: 16px;
}
.notice-bottom-popular-box {
grid-column: 2 / 3;
}
.notice-bottom-popular-card {
padding: 24px;
background-color: rgba(255, 255, 255, 1);
}
.notice-bottom-popular-card-box {
display: flex;
flex-direction: column;
gap: 24px;
}
.notice-bottom-popular-card-title {
color: rgba(0, 0, 0, 1);
font-size: 24px;
font-weight: 500;
line-height: 140%;
}
.notice-bottom-popular-card-contents {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.notice-bottom-pagination-box {
margin-top: 60px;
display: flex;
justify-content: center;
}

View File

@ -0,0 +1,162 @@
import React, { ChangeEvent, useEffect, useRef, useState } from "react";
import "./style.css";
import { useBoardStore, useLoginuserStore } from "stores";
import { MAIN_PATH } from "constant";
import { useNavigate } from "react-router-dom";
import { useCookies } from "react-cookie";
// component 게시물 작성화면 컴포넌트 //
export default function NoticeWrite() {
// state: 제목 영역 요소 참조 상태 //
const titleRef = useRef<HTMLTextAreaElement | null>(null);
// state: 본문 영역 요소 참조 상태 //
const contentRef = useRef<HTMLTextAreaElement | null>(null);
// state: 이미지 입력 요소 참조 상태 //
const imageInputRef = useRef<HTMLInputElement | null>(null);
// state: 게시물 상태 //
const { title, setTitle } = useBoardStore();
const { content, setContent } = useBoardStore();
const { boardImageFileList, setBoardImageFileList } = useBoardStore();
const { resetBoard } = useBoardStore();
// state: 쿠키 상태 //
const [cookies, setCookies] = useCookies();
// state: 게시물 이미지 미리보기 url 상태 //
const [imageUrls, setImageUrls] = useState<string[]>([]);
// function: 네비게이트 함수 //
const navigate = useNavigate();
// event handler: 제목 변경 이벤트 처리 //
const onTitleChangeHandler = (event: ChangeEvent<HTMLTextAreaElement>) => {
const { value } = event.target;
setTitle(value);
if (!titleRef.current) return;
titleRef.current.style.height = "auto";
titleRef.current.style.height = `${titleRef.current.scrollHeight}px`;
};
// event handler: 내용 변경 이벤트 처리 //
const onContentChangeHandler = (event: ChangeEvent<HTMLTextAreaElement>) => {
const { value } = event.target;
setContent(value);
if (!contentRef.current) return;
contentRef.current.style.height = "auto";
contentRef.current.style.height = `${contentRef.current.scrollHeight}px`;
};
// event handler: 이미지 변경 이벤트 처리 //
const onImageChangeHandler = (event: ChangeEvent<HTMLInputElement>) => {
if (!event.target.files || !event.target.files.length) return;
const file = event.target.files[0];
/* 미리보기를 위함 */
const imageUrl = URL.createObjectURL(file);
const newimageUrls = imageUrls.map((item) => item);
newimageUrls.push(imageUrl);
setImageUrls(newimageUrls);
/* 파일 업로드를 위함 */
const newBoardImageFileList = boardImageFileList.map((item) => item);
newBoardImageFileList.push(file);
setBoardImageFileList(newBoardImageFileList);
if (!imageInputRef.current) return;
imageInputRef.current.value = "";
};
// event handler: 이미지 업로드 버튼 클릭 이벤트 처리 //
const onImageUploadButtonClickHandler = () => {
if (!imageInputRef.current) return;
imageInputRef.current.click();
};
// event handler: 이미지 닫기 버튼 클릭 이벤트 처리 //
const onImageCloseButtonClickHandler = (deleteindex: number) => {
if (!imageInputRef.current) return;
imageInputRef.current.value = "";
const newImageUrls = imageUrls.filter(
(url, index) => index !== deleteindex
);
setImageUrls(newImageUrls);
const newBoardImageFileList = boardImageFileList.filter(
(file, index) => index !== deleteindex
);
setBoardImageFileList(newBoardImageFileList);
};
// effect: 마운트시 실행할 함수 //
useEffect(() => {
const accessToken = cookies.accessToken;
if (!accessToken) {
navigate(MAIN_PATH());
return;
}
resetBoard();
}, []);
// render 작성화면 렌더링 //
return (
<div id="notice-write-wrapper">
<div className="notice-write-container">
<div className="notice-write-box">
<div className="notice-write-title-box">
<textarea
ref={titleRef}
className="notice-write-title-textarea"
rows={1}
placeholder="제목을 작성해주세요."
value={title}
onChange={onTitleChangeHandler}
/>
</div>
<div className="divider"></div>
<div className="notice-write-content-box">
<textarea
ref={contentRef}
className="notice-write-content-textarea"
placeholder="본문을 작성해주세요."
value={content}
onChange={onContentChangeHandler}
/>
<div
className="icon-button"
onClick={onImageUploadButtonClickHandler}
>
<div className="icon image-box-light-icon"></div>
</div>
<input
ref={imageInputRef}
type="file"
accept="image/*"
style={{ display: "none" }}
onChange={onImageChangeHandler}
/>
</div>
<div className="notice-write-images-box">
{imageUrls.map((imageUrl, index) => (
<div className="notice-write-image-box">
<img className="notice-write-image" src={imageUrl} />
<div
className="icon-button image-close"
onClick={() => onImageCloseButtonClickHandler(index)}
>
<div className="icon close-icon"></div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,83 @@
#notice-write-wrapper {
border-top: 1px solid rgba(0, 0, 0, 0.2);
display: flex;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
}
.notice-write-container {
padding: 100px 24px;
width: 996px;
min-height: 1952px;
background-color: rgba(255, 255, 255, 1);
}
.notice-write-box {
display: flex;
flex-direction: column;
gap: 40px;
}
.notice-write-title-box {
width: 100%;
}
.notice-write-title-textarea {
width: 100%;
border: none;
outline: none;
background: none;
resize: none;
color: rgba(0, 0, 0, 1);
font-size: 32px;
font-weight: 500;
line-height: 140%;
}
.notice-write-content-box {
width: 100%;
display: flex;
gap: 16px;
}
.notice-write-content-textarea {
flex: 1;
border: none;
outline: none;
background: none;
resize: none;
color: rgba(0, 0, 0, 0.7);
font-size: 19px;
font-weight: 500;
line-height: 150%;
}
.notice-write-images-box {
width: 100%;
display: flex;
flex-direction: column;
gap: 20px;
}
.notice-write-image-box {
width: 100%;
position: relative;
}
.notice-write-image {
width: 100%;
}
.image-close {
position: absolute;
top: 20px;
right: 20px;
}

View File

@ -4,9 +4,9 @@
"settings": {
"width": 2000,
"height": 2000,
"scrollTop": -482.7981,
"scrollLeft": -512.5388,
"zoomLevel": 0.7,
"scrollTop": -166.6666,
"scrollLeft": -800,
"zoomLevel": 0.9,
"show": 431,
"database": 4,
"databaseName": "",
@ -380,18 +380,18 @@
"tableId": "5jPYxY_W7XcoLCO--IWtf",
"name": "user_id",
"comment": "유저 아이디",
"dataType": "VARCHAR2(20)",
"dataType": "VARCHAR(20)",
"default": "",
"options": 10,
"ui": {
"keys": 1,
"widthName": 60,
"widthComment": 65,
"widthDataType": 81,
"widthDataType": 75,
"widthDefault": 60
},
"meta": {
"updateAt": 1726126271711,
"updateAt": 1727066200235,
"createAt": 1725003493803
}
},
@ -860,18 +860,18 @@
"tableId": "WU-vbaja1NU7Fy0EQK5uE",
"name": "user_id",
"comment": "유저 아이디",
"dataType": "VARCHAR2(20)",
"dataType": "VARCHAR(20)",
"default": "",
"options": 8,
"ui": {
"keys": 2,
"widthName": 60,
"widthComment": 65,
"widthDataType": 81,
"widthDataType": 75,
"widthDefault": 60
},
"meta": {
"updateAt": 1726126271711,
"updateAt": 1727066200235,
"createAt": 1726030193552
}
},
@ -940,18 +940,18 @@
"tableId": "Qf7oberqyXg9rwB2NXnFB",
"name": "user_id",
"comment": "유저 아이디",
"dataType": "VARCHAR2(20)",
"dataType": "VARCHAR(20)",
"default": "",
"options": 8,
"ui": {
"keys": 2,
"widthName": 60,
"widthComment": 65,
"widthDataType": 81,
"widthDataType": 75,
"widthDefault": 60
},
"meta": {
"updateAt": 1726126271711,
"updateAt": 1727066200235,
"createAt": 1726031243540
}
},
@ -1300,18 +1300,18 @@
"tableId": "7E1EZCsr9O350-mnaBiSH",
"name": "user_id",
"comment": "유저 아이디",
"dataType": "VARCHAR2(20)",
"dataType": "VARCHAR(20)",
"default": "",
"options": 8,
"ui": {
"keys": 2,
"widthName": 60,
"widthComment": 65,
"widthDataType": 81,
"widthDataType": 75,
"widthDefault": 60
},
"meta": {
"updateAt": 1726126271711,
"updateAt": 1727066200235,
"createAt": 1726125691896
}
},
@ -1400,18 +1400,18 @@
"tableId": "KOhqpIaGA2yW6g0RT1uDd",
"name": "stat",
"comment": "출장, 휴가 상태",
"dataType": "VARCHAR2(10)",
"dataType": "VARCHAR(10)",
"default": "",
"options": 8,
"ui": {
"keys": 0,
"widthName": 60,
"widthComment": 83,
"widthDataType": 81,
"widthDataType": 75,
"widthDefault": 60
},
"meta": {
"updateAt": 1726126768873,
"updateAt": 1727066203905,
"createAt": 1726125976668
}
},
@ -1420,18 +1420,18 @@
"tableId": "KOhqpIaGA2yW6g0RT1uDd",
"name": "st_dt",
"comment": "시작 날짜",
"dataType": "TIMESTAMP",
"dataType": "DATETIME",
"default": "",
"options": 0,
"ui": {
"keys": 0,
"widthName": 60,
"widthComment": 60,
"widthDataType": 65,
"widthDataType": 60,
"widthDefault": 60
},
"meta": {
"updateAt": 1726126783509,
"updateAt": 1727066242335,
"createAt": 1726125982429
}
},
@ -1440,18 +1440,18 @@
"tableId": "KOhqpIaGA2yW6g0RT1uDd",
"name": "end_dt",
"comment": "종료 날짜",
"dataType": "TIMESTAMP",
"dataType": "DATETIME",
"default": "",
"options": 0,
"ui": {
"keys": 0,
"widthName": 60,
"widthComment": 60,
"widthDataType": 65,
"widthDataType": 60,
"widthDefault": 60
},
"meta": {
"updateAt": 1726126786557,
"updateAt": 1727066245047,
"createAt": 1726125982709
}
},
@ -1460,18 +1460,18 @@
"tableId": "KOhqpIaGA2yW6g0RT1uDd",
"name": "user_id",
"comment": "유저 아이디",
"dataType": "VARCHAR2(20)",
"dataType": "VARCHAR(20)",
"default": "",
"options": 8,
"ui": {
"keys": 2,
"widthName": 60,
"widthComment": 65,
"widthDataType": 81,
"widthDataType": 75,
"widthDefault": 60
},
"meta": {
"updateAt": 1726126271711,
"updateAt": 1727066200235,
"createAt": 1726125996333
}
},
@ -1540,18 +1540,18 @@
"tableId": "ingpkJqg8oHlrFUxaftAa",
"name": "user_id",
"comment": "유저 아이디",
"dataType": "VARCHAR2(20)",
"dataType": "VARCHAR(20)",
"default": "",
"options": 8,
"ui": {
"keys": 2,
"widthName": 60,
"widthComment": 65,
"widthDataType": 81,
"widthDataType": 75,
"widthDefault": 60
},
"meta": {
"updateAt": 1726126414371,
"updateAt": 1727066200235,
"createAt": 1726126414371
}
},
@ -1901,7 +1901,7 @@
-1,
{
"name": 1726033380026,
"dataType": 1726126271710,
"dataType": 1727066200234,
"comment": 1726033380026,
"options(notNull)": 1726033380026,
"options(primaryKey)": 1726029927289,
@ -2152,7 +2152,7 @@
{
"options(notNull)": 1726030193551,
"name": 1726030193551,
"dataType": 1726126271710,
"dataType": 1727066200234,
"default": 1726030193551,
"comment": 1726030193551
}
@ -2205,7 +2205,7 @@
{
"options(notNull)": 1726031243537,
"name": 1726031243537,
"dataType": 1726126271710,
"dataType": 1727066200234,
"default": 1726031243537,
"comment": 1726031243537
}
@ -2414,7 +2414,7 @@
{
"options(notNull)": 1726125691894,
"name": 1726125691894,
"dataType": 1726126271710,
"dataType": 1727066200234,
"default": 1726125691894,
"comment": 1726125691894
}
@ -2479,7 +2479,7 @@
-1,
{
"name": 1726126668737,
"dataType": 1726126213111,
"dataType": 1727066203905,
"options(notNull)": 1726126297885,
"options(primaryKey)": 1726126634772,
"comment": 1726126768873
@ -2491,7 +2491,7 @@
-1,
{
"name": 1726126122745,
"dataType": 1726126233140,
"dataType": 1727066242334,
"comment": 1726126783509
}
],
@ -2501,7 +2501,7 @@
-1,
{
"name": 1726126128432,
"dataType": 1726126234967,
"dataType": 1727066245046,
"comment": 1726126786556
}
],
@ -2512,7 +2512,7 @@
{
"options(notNull)": 1726125996330,
"name": 1726125996330,
"dataType": 1726126271710,
"dataType": 1727066200234,
"default": 1726125996330,
"comment": 1726125996330
}
@ -2566,7 +2566,7 @@
{
"options(notNull)": 1726126414369,
"name": 1726126414369,
"dataType": 1726126414369,
"dataType": 1727066200234,
"default": 1726126414369,
"comment": 1726126414369
}