3D 기반 웹 추억 저장 서비스
@@ -16,9 +18,9 @@
-
+[✨ <별 하나에 글 하나> 사용해보기](https://www.xn--bj0b03z.site/)
-[wiki 바로가기](https://github.com/boostcampwm2023/web16-B1G1/wiki)
+[🔗 wiki 바로가기](https://github.com/boostcampwm2023/web16-B1G1/wiki)
@@ -29,16 +31,17 @@
### [1. 프로젝트 소개](#%EF%B8%8F-프로젝트-소개)
- [<별 하나에 글 하나>를 만들게 된 계기](#별-하나에-글-하나를-만들게-된-계기)
-- [기능 설명](#기능-설명)
+- [주요 기능 설명](#주요-기능-설명)
- [프로젝트 실행 방법](#프로젝트-실행-방법)
### [2. 기술 스택](#%EF%B8%8F-기술-스택)
### [3. 기술적 경험](#-기술적-경험)
+
- [FE](#FE)
- [R3F Camera](#r3f-camera)
- [성능 최적화](#성능-최적화)
- - [FSD 아키텍처](#fsd-아키텍처)
+ - [FSD 아키텍처](#fsd-아키텍처)
- [BE](#BE)
- [TDD, e2e 및 유닛 테스트](#tdd-e2e-및-유닛-테스트)
- [인증/인가](#인증인가)
@@ -48,6 +51,7 @@
- [admin 페이지 구현](#admin-페이지-구현)
### [4. 팀원 소개](#%EF%B8%8F-팀원-소개)
+
- [J010 김가은](#-j010-김가은-fe)
- [J016 김동민](#-j016-김동민-fe)
- [J053 박재하](#-j053-박재하-be)
@@ -74,37 +78,11 @@
-## 기능 설명
-
-### [ 랜딩페이지 ]
-
-
-
-- 왼쪽 위의 버튼을 이용해 배경음악을 끄고 켤 수 있습니다.
-- 마우스의 움직임에 따라 배경의 은하가 움직입니다.
-- f9를 눌러 전체화면으로 변경할 수 있습니다.
-
-### [ 회원가입 ]
+## 주요 기능 설명
-
-
+wiki에서 더 많은 기능을 살펴볼 수 있습니다.
-- 중복확인, 형식 검사 등을 거친 올바른 아이디 / 비밀번호 / 닉네임으로 회원가입할 수 있습니다.
-
-### [ 로그인 ]
-
-
-
-- 로그인 후 홈화면으로 이동할 때 화면전환 애니메이션이 발생합니다.
-- 네이버 , 깃허브, 구글 소셜 로그인이 가능합니다.
-- 오른쪽 위 '예시 은하 구경하기' 버튼을 누르면 로그인하지 않고도 은하를 구경해볼 수 있습니다.
-
-### [ 코치마크 ]
-
-
-
-- 사용자에게 기본적인 서비스 사용법을 알려주는 기능입니다.
-- 첫 로그인 시에는 기본으로 뜨며, 이후에는 하단바의 물음표 버튼을 눌러 다시 볼 수 있습니다.
+[🔗 wiki 프로젝트 소개 바로가기](https://github.com/boostcampwm2023/web16-B1G1/wiki)
### [ 글 조회 ]
@@ -140,15 +118,6 @@
- 오른쪽 위의 되돌리기 버튼을 누르면 수정 이전의 내 은하 스타일로 돌아갑니다.
- 왼쪽 아래의 초기화 버튼을 누르면 기본 은하 스타일로 돌아갑니다.
-### [ 사용자 우주 설정 ]
-
-
-
-- 별의 밝기를 조절할 수 있습니다.
-- 블러효과를 주어 우주에 흐림 효과를 줄 수 있습니다.
-- 마우스 휠 속도를 조절할 수 있습니다.
-- 우주의 소유주와 관계없이 내가 보는 화면에만 적용되는 속성입니다.
-
### [ 은하 공유 ]
@@ -219,14 +188,20 @@ yarn workspace server start:dev
### R3F Camera
-3D 공간 상에서 카메라는 사용자의 시야와 같습니다.
-그래서 카메라 움직임은 사용자 경험에 직결됩니다.
+3D 공간 상에서 카메라는 사용자의 시점입니다.
+그렇기 때문에 카메라 움직임은 사용자 경험에 직결됩니다.
저희는 `자연스러운 카메라 움직임`을 만들어내 사용자 경험을 향상시키기 위해 여러 과정을 거쳤습니다.
저희 서비스에서 별을 클릭하면 해당 별을 바라보도록 해야 합니다.
처음에는 카메라의 위치는 그대로 둔 채 시야만 회전하도록 하는 `회전 운동`의 방식을 사용했습니다.
-그 다음에는 해당 별로 가까워지는 `직선 운동`으로 변경했습니다.
-결과적으로는 회전운동과 직선운동의 장점을 합친 `포물선 운동` 방식을 적용해 훨씬 자연스러운 구현해냈습니다.
+처음 `회전 운동` 방식을 적용해본 결과, 별을 바꿀때마다 별과 카메라 사이의 거리를 직접 조정해 줘야 한다는 문제가 있었습니다.
+
+그래서 별과 카메라 사이 거리를 유지한 채 별을 향해 `직선 운동` 하도록 변경했습니다.
+이 방식은 `회전 운동`에 비해 사용하기 편했으나, 움직임이 너무 뻣뻣했기에 더 부드러운 모션을 추가하면 좋겠다는 생각을 하게 되었습니다.
+
+많은 고민 끝에 회전 운동처럼 별을 향해 회전하고 직선 운동처럼 별에 다가가도록 하여 '포물선 운동'을 만들어 냈습니다.
+`포물선 운동`은 회전 운동의 장점인 자연스러운 움직임과 직선 운동의 장점인 직관적인 움직임을 모두 가졌습니다.
+이러한 이유로 저희는 `포물선 운동`을 적용하게 되었습니다.
- 직선 운동하는 카메라
@@ -236,62 +211,73 @@ yarn workspace server start:dev
-하지만 아직도 문제가 남아있었습니다.
-거리가 먼 별에 가까워지기까지 너무 많은 시간이 걸렸고, 멀리 있는 별은 너무 작게 보였습니다.
-어찌보면 당연한 이야기일 수 있지만, 서비스 특성상 사용자 입장에서는 불편할 수 있는 요소였습니다.
+하지만 아직 멀리 있는 별이 너무 작게 보이는 문제가 남아있었습니다.
+어찌보면 당연한 이야기일 수 있지만, 서비스 특성상 사용자 입장에서 불편한 요소였고 시각적으로 좋지 않았습니다.
+그래서 거리에 비해서 물체가 커 보이게 처리해 멀리 있는 별이 너무 작아보이지 않도록 했습니다.
-그래서 멀리 이동할 때는 좀 더 빠르게, 가까이 이동할 때는 좀 더 느리게 이동하는 움직임을 구현했습니다.
-또 거리에 비해서 물체가 커 보이게 처리함으로써 멀리 있는 별도 너무 작아보이지 않게 했습니다.
+그랬더니 거리가 먼 별이 겉보기보다 멀리 위치하게 되는 문제가 발생했습니다.
+사용자가 그 별로 이동하는데 예상하는 것보다 많은 시간이 소요되었습니다.
+이 문제를 해결하기 위해 멀리 이동할 때는 좀 더 빠르게, 가까이 이동할 때는 좀 더 느리게 이동하도록 처리했습니다.
### 성능 최적화
-한 화면에 매우 많은 오브젝트를 보여줘야 하는 프로젝트 특성상 실행 환경에 많은 영향을 받기 때문에 최적화가 필수적이었습니다.
-
-이에 여러 최적화 방법 중 우리 프로젝트에 적용하기 가장 적합한 `Instancing`과 `Performace Moritoring`을 사용해 최적화를 진행했습니다.
+저희는 은하를 만들기 위해 수많은 별 오브젝트들을 화면에 띄워야 했습니다.
+하지만 별 개수를 늘릴수록 화면이 더 버벅이기 시작했습니다.
+별 개수를 줄이면 시각적으로 좋지 않았기에, 저희는 별 개수를 유지하면서도 화면이 버벅이지 않도록 최적화를 시도하게 되었습니다.
1. Instancing
- CPU가 GPU에게 무엇을 어떻게 그릴지 지시하는 `Draw Call`은 단순해 보이지만 상당히 무거운 작업입니다. 일반적인 컴퓨터 환경에서 Draw Call이 대략 1000회 넘어가 프레임 드랍이 생긴다는 점을 고려하면, 배경별만 4000개인 우리 프로젝트는 이 `Draw Call`을 줄여줘야 했습니다.
+ 저희가 선택한 첫 번째 최적화 방식은 `Instancing`이었습니다.
+
+ CPU가 GPU에게 무엇을 어떻게 그릴지 지시하는 `Draw Call`은 단순해 보이지만 상당히 무거운 작업입니다. 일반적인 컴퓨터 환경에서 Draw Call이 대략 1000회 넘어가면 프레임 드랍이 생긴다고 합니다. 은하를 구성하는 별 오브젝트만 4000개인 저희 프로젝트에서 이러한 `Draw Call`을 줄이는 것이 중요햐다고 생각했습니다.
+
+ 이를 위해 사용한 방식이 `Instancing`으로, 동일한 오브젝트를 여러 번 그리는 경우 이를 한번에 처리하도록 하는 방식입니다. 저희는 이를 `InstancedMesh`를 사용해 구현했습니다. 이 방식을 통해 은하를 구성하는 별을 종류별로 묶어줌으로써 4000개의 오브젝트를 13개의 인스턴스로 줄일 수 있었습니다. 이렇게 `Draw Call`에 의한 CPU 병목 현상을 해결했습니다.
- 이를 위해 사용한 방식이 `Instancing`으로, 동일한 오브젝트를 여러 번 그리는 경우 이를 한번에 처리하도록 하는 방식입니다. 이 방식을 통해 배경별을 종류별로 묶어줌으로써 4000개의 오브젝트를 13개의 오브젝트로 줄였습니다. 이렇게 `Draw Call`에 의한 CPU 병목 현상을 해결했습니다.
+
+
+하지만 금요일 프로젝트 현황 공유 시간 때 '처음으로 맥북 팬 소리를 들었어요', '컴터가 안좋아서 그런지 느려요ㅠㅜㅠ' 같은 피드백을 들으면서 추가적인 최적화 작업의 필요성을 느꼈습니다.
+
+
- 하지만 금요일 프로젝트 현황 공유 시간 때 '처음으로 맥북 팬 소리를 들었어요', '컴터가 안좋아서 그런지 느려요ㅠㅜㅠ' 같은 피드백을 들으면서 추가적인 최적화 작업에 대한 필요성을 느꼈습니다.
+2. Performance Monitoring
-2. Performance Monitoring
+ 피드백을 받은 이후 선택한 것은 `Performance Monitoring`입니다. 다양한 최적화 방식이 있었으나 프로젝트에서 사용하는 대부분의 오브젝트가 매우 단순한 형태라 그리 효과적이지 않았습니다. 이에 선택한 방법이 `Performance Monitoring`으로, 실시간으로 웹의 퍼포먼스를 모니터링해 이를 반영하는 방식입니다.
- 피드백을 받은 이후 선택한 것은 Performance Monitoring을 사용한 방식입니다. 다양한 최적화 방식이 있었으나 프로젝트에서 사용하는 대부분의 오브젝트가 매우 단순한 형태라 그리 효과적이지 않았습니다. 이에 선택한 방법이 `Performance Monitoring`으로, 실시간으로 웹의 퍼포먼스를 확인해 이를 반영하는 방식입니다.
+ react-three/drei 라이브러리의 `Performance Monitor`를 통해 웹의 퍼포먼스를 모니터링합니다. 그리고 퍼포먼스가 좋지 않은 경우 Canvas의 `Device Pixel Ratio`을 최대 0.5까지 낮춥니다. 은하의 해상도를 낮추어 프레임 드랍을 해결하는 방식입니다. 이렇게 CPU만 고려하던 1번 방식에서 나아가 GPU의 부담까지 덜어주는 방식을 추가함으로써 더 최적화된 서비스를 만들 수 있었습니다.
- 퍼포먼스가 좋지 않은 경우 (프레임 드랍 시) `Device Pixel Ratio`을 최대 0.5까지 낮추어 은하의 해상도가 낮아집니다. 이렇게 CPU만 고려하던 1번 방식에서 나아가 GPU의 부담까지 덜어주는 방식을 추가함으로써 더 최적화된 서비스를 만들 수 있었습니다.
+ 아래 사진 중 왼쪽은 최고 해상도인 경우이고, 오른쪽은 최저 해상도인 경우입니다.
- 아래 사진 중 왼쪽은 최고 해상도인 경우이고, 오른쪽은 최저 해상도인 경우입니다.
+
+
-
-
+ 아래 사진은 메모리 사용량을 비교한 것으로, Performance Monitoring 최적화 전 13.46GB였던 메모리 사용량이 최적화 후 12.50GB까지 감소했습니다.
- 아래 사진은 메모리 사용량을 비교한 것으로, Performance Monitoring 최적화 전 13.46GB였던 메모리 사용량이 최적화 후 12.50GB까지 감소했습니다.
+
+
-
-
+ 아래 사진은 퍼포먼스를 비교한 것으로, GPU 전력 사용량이 0.91 에서 0.62로 감소했고 GPU 사용률이 66에서 51로 감소했습니다.
+
+
+
### FSD 아키텍처
-프로젝트를 진행함에 따라 파일 분리와 폴더 구조에 대한 명확한 원칙이 필요해졌습니다.
-그래서 팀원들이 함께 폴더 구조에 대해 조사해보았고, 결과적으로 [FSD(Feature-Sliced Design)](https://feature-sliced.design/) 아키텍처를 적용하게 되었습니다.
+프로젝트를 진행함에 따라 파일들이 점점 많아졌고, 파일 분리와 폴더 구조에 대한 명확한 원칙이 필요해졌습니다.
+그래서 팀원들이 함께 여러 폴더 구조와 아키텍쳐들에 대해 조사해보았고, 결과적으로 [FSD(Feature-Sliced Design)](https://feature-sliced.design/) 아키텍처를 적용하게 되었습니다.
-규모가 작은 저희 프로젝트와 달리, FSD 방식은 폴더를 세세하게 나누는 만큼 규모가 큰 프로젝트에 적합하다는 생각도 했습니다.
-하지만 해당 방식의 장점이 매력적으로 다가오기도 했고, 이 프로젝트는 학습의 목적이 크기 때문에 팀원들 모두 새로운 폴더구조를 적용해보고 싶어했습니다.
+저희 프로젝트는 상대적으로 규모가 작은 편인데, FSD 방식은 폴더를 세세하게 나누는 만큼 규모가 큰 프로젝트에 적합하다는 생각도 했습니다.
+하지만 프로젝트를 분할하여 정복하는 해당 방식의 장점이 매력적으로 다가오기도 했고, 이 프로젝트는 학습의 목적이 크기 때문에 팀원들 모두 새로운 폴더구조를 적용해보고 싶어했습니다.
_출처: https://feature-sliced.design/_
-FSD 아키텍처는 app, pages, widgets, features, entities, shared라는 6개의 `Layer`로 이루어져있습니다. 그리고 각각의 `Layer`는 `Slice`들로 이루어져있고, 그 `Slice`는 `Segment`로 이루어져있습니다.
-저희 팀은 이렇게 각자의 역할이 분명한 폴더구조를 적용해봄으로써 모듈을 만들 때 각 모듈의 역할을 명확히 정의하게 되었습니다.
-또 독립적으로 기능하는 `Slice` 단위로 개발함으로써 유지보수성을 높였습니다.
+FSD 아키텍처는 app, pages, widgets, features, entities, shared라는 6개의 `Layer`로 이루어져있습니다. 그리고 각각의 `Layer`는 `Slice`들로 이루어져있고, 그 `Slice`는 `Segment`로 이루어져있습니다. 하위요소들을 조합하여 상위 요소를 구성하는 방식으로, 이 매커니즘이 저희에게 굉장히 매력적으로 다가왔습니다.
+이렇게 각자의 역할이 분명한 폴더구조를 적용해봄으로써 모듈을 만들 때 각 모듈의 역할을 명확히 정의하게 되었습니다. 또한 하위 요소들이 모두 개별적으로 기능할 수 있기 때문에 훨씬 유지보수성이 높은 코드를 작성할 수 있게 되었습니다.
-아래는 저희의 폴더구조입니다.
+아래는 저희 프로젝트의 폴더구조입니다.
```
📦src
@@ -456,14 +442,12 @@ Interceptor, Exception Filter 등 학습을 하고 백엔드 코드에 적용을
### admin 페이지 구현
리액트를 경험해보고 싶어서 Vite + React + TS를 활용해 간단한 admin 페이지를 만들어보았습니다.
-admin용 계정 정보를 설정하고, 게시글 관리 및 컴퓨터 자원 사용량, 에러 로그의 차트를 볼 수 있습니다.
+admin용 계정 정보를 설정하고, 게시글 관리 및 컴퓨터 자원 사용량, 에러 로그의 차트를 볼 수 있습니다.
![image](https://github.com/boostcampwm2023/web16-B1G1/assets/101378867/780802e8-94f2-427d-b5c5-96e4f03dea17)
![image](https://github.com/boostcampwm2023/web16-B1G1/assets/101378867/94877604-5f54-4807-8570-d7d7aa699c65)
-
-
# 🏃♂️ 팀원 소개
diff --git a/packages/client/src/pages/Home/Home.tsx b/packages/client/src/pages/Home/Home.tsx
index b8862df..158c873 100644
--- a/packages/client/src/pages/Home/Home.tsx
+++ b/packages/client/src/pages/Home/Home.tsx
@@ -95,11 +95,10 @@ export default function Home() {
<>
{status === 'new' && }
- {isSwitching !== 'end' && (
-
- )}
{text && {text}}
+
+
diff --git a/packages/client/src/shared/lib/types/post.ts b/packages/client/src/shared/lib/types/post.ts
index efaddd6..fb8738b 100644
--- a/packages/client/src/shared/lib/types/post.ts
+++ b/packages/client/src/shared/lib/types/post.ts
@@ -5,3 +5,5 @@ export interface PostData {
images: string[];
like_cnt?: number;
}
+
+export type TextStateTypes = 'DEFAULT' | 'INVALID' | 'OVER';
diff --git a/packages/client/src/widgets/postModal/ui/PostModal.tsx b/packages/client/src/widgets/postModal/ui/PostModal.tsx
index 4bfcfa9..dad3bf5 100644
--- a/packages/client/src/widgets/postModal/ui/PostModal.tsx
+++ b/packages/client/src/widgets/postModal/ui/PostModal.tsx
@@ -6,11 +6,13 @@ import { useLocation, useNavigate, useParams } from 'react-router-dom';
import remarkGfm from 'remark-gfm';
import { instance } from 'shared/apis';
import { useCheckNickName, useFetch, useRefresh } from 'shared/hooks';
-import { PostData } from 'shared/lib';
+import { PostData, TextStateTypes } from 'shared/lib';
import { useCameraStore, useToastStore, useViewStore } from 'shared/store';
import { AlertDialog, Button, Input, Modal, TextArea } from 'shared/ui';
import { deletePost } from '../api/deletePost';
import ImageSlider from './ImageSlider';
+import { Caption } from 'shared/styles';
+import { css } from '@emotion/react';
export default function PostModal() {
const [deleteModal, setDeleteModal] = useState(false);
@@ -19,6 +21,8 @@ export default function PostModal() {
const [title, setTitle] = useState('');
const [isDeleteButtonDisabled, setIsDeleteButtonDisabled] = useState(false);
const [isSaveButtonDisabled, setIsSaveButtonDisabled] = useState(false);
+ const [titleState, setTitleState] = useState('DEFAULT');
+ const [contentState, setContentState] = useState('DEFAULT');
const { setToast } = useToastStore();
const { setView } = useViewStore();
@@ -44,7 +48,6 @@ export default function PostModal() {
const formData = new FormData();
formData.append('title', title);
formData.append('content', content);
-
try {
await instance({
url: `/post/${postId}`,
@@ -105,6 +108,8 @@ export default function PostModal() {
buttonType="CTA-icon"
type="button"
onClick={() => {
+ if (title === '') return setTitleState('INVALID');
+ if (content === '') return setContentState('INVALID');
setIsEdit(false);
handleEditSave();
}}
@@ -160,21 +165,44 @@ export default function PostModal() {
)}
{isEdit ? (
- setTitle(e.target.value)}
- />
-
) : (
@@ -203,6 +231,45 @@ export default function PostModal() {
);
}
+const ContentContainer = styled.div<{ state: TextStateTypes }>`
+ border: 1px solid;
+ border-radius: 4px;
+ height: 75%;
+ ${({ state, theme: { colors } }) => {
+ if (state === 'DEFAULT') return;
+
+ return css`
+ border-color: ${colors.text.warning};
+
+ &:focus {
+ border-color: ${colors.text.warning};
+ }
+
+ &:hover {
+ border-color: ${colors.text.warning};
+ }
+ `;
+ }};
+`;
+
+const TitleInput = styled(Input)<{ state: TextStateTypes }>`
+ ${({ state, theme: { colors } }) => {
+ if (state === 'DEFAULT') return;
+
+ return css`
+ border-color: ${colors.text.warning};
+
+ &:focus {
+ border-color: ${colors.text.warning};
+ }
+
+ &:hover {
+ border-color: ${colors.text.warning};
+ }
+ `;
+ }};
+`;
+
const PostModalLayout = styled(Modal)`
transform: translate(-10%, -50%);
`;
@@ -263,3 +330,15 @@ const ButtonContainer = styled.div`
display: flex;
gap: 8px;
`;
+
+const Message = styled.p`
+ position: absolute;
+ margin: 4px 0 0 0;
+ color: ${({ theme: { colors } }) => colors.text.warning};
+
+ ${Caption}
+`;
+
+const TitleContainer = styled.div`
+ margin-bottom: 30px;
+`;
diff --git a/packages/client/src/widgets/warpScreen/WarpScreen.tsx b/packages/client/src/widgets/warpScreen/WarpScreen.tsx
index d2232af..0de1210 100644
--- a/packages/client/src/widgets/warpScreen/WarpScreen.tsx
+++ b/packages/client/src/widgets/warpScreen/WarpScreen.tsx
@@ -36,7 +36,11 @@ export default function WarpScreen({ isSwitching, setIsSwitching }: PropsType) {
zIndex: 999,
backgroundColor: theme.colors.background.bdp04,
};
- if (isSwitching === 'fade') return ;
+
+ if (isSwitching === 'end') return null;
+
+ if (isSwitching === 'fade')
+ return setIsSwitching('end')} />;
return (
diff --git a/packages/client/src/widgets/warpScreen/ui/SpaceWarp.tsx b/packages/client/src/widgets/warpScreen/ui/SpaceWarp.tsx
index 009498c..5e9728a 100644
--- a/packages/client/src/widgets/warpScreen/ui/SpaceWarp.tsx
+++ b/packages/client/src/widgets/warpScreen/ui/SpaceWarp.tsx
@@ -13,7 +13,7 @@ import {
} from '../lib/constants';
import { WarpStateType } from 'shared/lib';
-const geSpaceWarpLinesInfo = () => {
+const getSpaceWarpLinesInfo = () => {
const positions = Array.from({ length: SPACE_WARP_LINES_NUM }, () => {
const x = getRandomFloat(SPACE_WARP_XZ_MIN, SPACE_WARP_XZ_MAX);
const y = getRandomFloat(SPACE_WARP_Y_MIN, SPACE_WARP_Y_MAX);
@@ -38,10 +38,10 @@ interface PropsType {
}
export default function SpaceWarp({ setIsSwitching }: PropsType) {
- const [positions, colors] = useMemo(() => geSpaceWarpLinesInfo(), []);
+ const [positions, colors] = useMemo(() => getSpaceWarpLinesInfo(), []);
useFrame((state, delta) => {
- if (state.camera.position.y <= 0) {
+ if (state.camera.position.y <= SPACE_WARP_Y_MIN) {
state.scene.background = new THREE.Color(0xffffff);
setIsSwitching('fade');
return;
diff --git a/packages/client/src/widgets/writingModal/ui/WritingModal.tsx b/packages/client/src/widgets/writingModal/ui/WritingModal.tsx
index 9378d5b..39801d5 100644
--- a/packages/client/src/widgets/writingModal/ui/WritingModal.tsx
+++ b/packages/client/src/widgets/writingModal/ui/WritingModal.tsx
@@ -7,8 +7,7 @@ import { usePostStore, useViewStore } from 'shared/store';
import { Caption } from 'shared/styles';
import { AlertDialog, Button, Input, Modal, TextArea } from 'shared/ui';
import Images from './Images';
-
-type TextStateTypes = 'DEFAULT' | 'INVALID' | 'OVER';
+import { TextStateTypes } from 'shared/lib';
export default function WritingModal() {
const [titleState, setTitleState] = useState('DEFAULT');
diff --git a/packages/server/src/board/dto/create-board.dto.ts b/packages/server/src/board/dto/create-board.dto.ts
index 763206e..37b6558 100644
--- a/packages/server/src/board/dto/create-board.dto.ts
+++ b/packages/server/src/board/dto/create-board.dto.ts
@@ -4,7 +4,7 @@ import { IsJSON, IsNotEmpty, IsString, MaxLength } from 'class-validator';
export class CreateBoardDto {
@IsNotEmpty({ message: '게시글 제목은 필수 입력입니다.' })
@IsString({ message: '게시글 제목은 문자열로 입력해야 합니다.' })
- @MaxLength(20, { message: '게시글 제목은 20자 이내로 입력해야 합니다.' })
+ @MaxLength(40, { message: '게시글 제목은 20자 이내로 입력해야 합니다.' })
@ApiProperty({
description: '게시글 제목',
example: 'test title',
@@ -14,7 +14,7 @@ export class CreateBoardDto {
@IsNotEmpty({ message: '게시글 내용은 필수 입력입니다.' })
@IsString({ message: '게시글 내용은 문자열로 입력해야 합니다.' })
- @MaxLength(1000, { message: '게시글 내용은 1000자 이내로 입력해야 합니다.' })
+ @MaxLength(2200, { message: '게시글 내용은 1000자 이내로 입력해야 합니다.' })
@ApiProperty({
description: '게시글 내용',
example: 'test content',
diff --git a/packages/server/src/exception-filter/http.exception-filter.ts b/packages/server/src/exception-filter/http.exception-filter.ts
index 760e0cb..9dd9c62 100644
--- a/packages/server/src/exception-filter/http.exception-filter.ts
+++ b/packages/server/src/exception-filter/http.exception-filter.ts
@@ -16,13 +16,14 @@ export class HttpExceptionFilter {
const response = context.getResponse();
const method = request.method;
const status = exception.getStatus();
+ const errorResponse = exception.getResponse() as any;
const now = new Date();
const korTime = new Date(now.getTime() + 9 * 3600 * 1000);
const exceptionData = {
path: `${method} ${request.url}`,
error: `${status} ${exception.name}`,
- message: exception.message,
+ message: errorResponse.message,
timestamp: korTime,
};
const saveException = new this.exceptionModel(exceptionData);
diff --git a/packages/server/src/interceptor/log.interceptor.ts b/packages/server/src/interceptor/log.interceptor.ts
index f98bddc..4f35716 100644
--- a/packages/server/src/interceptor/log.interceptor.ts
+++ b/packages/server/src/interceptor/log.interceptor.ts
@@ -50,7 +50,11 @@ export class LogInterceptor implements NestInterceptor {
errLog += ` ${userString}`;
}
Logger.error(errLog);
- Logger.error(error);
+ Logger.error(
+ `${error.getResponse()['statusCode']} ${
+ error.getResponse()['error']
+ } - ${error.getResponse()['message']}`,
+ );
throw error;
}),
tap(() => {