PJH's Community Site - Community(3)

박정호·2022년 12월 2일
0

Community Project

목록 보기
8/14
post-thumbnail

⭐️ Community Detail Page

이제 커뉴니티마다의 상세페이지를 생성시켜주자.



✔️ 상세페이지 생성

파일 생성
Next.js는 파일 기반 라루팅 동작 방식을 가진다. 따라서, pages의 하위 폴더들은 각각의 route에 해당하는 이름을 갖게되며, 마찬가지로 index.js 파일을 갖고 있다.
특히, route를 동적으로 구성하고 싶을 때는 [] 안에 이름을 정의한 컴포넌트 파일을 만들어서 구현할 수 있다.

[]안에는 보통 동적으로 구성하는 상세 페이지의 변수명을 넣는다. 지금과 같이 게시글의 상세페이지를 구현한다면 그 게시글의 고유의 name 또는 id가 있을 것이므로 [name].tsx 처럼 구현할 수 있는 것이다. (참고)



✔️ 상세페이지 UI 작성


const SubPage = () => {
  ...
  return (  <>
     {sub &&
           <>
              <div>
               <div>
                  {sub.bannerUrl ? (
                       <div onClick={() => openFileInput("banner")}></div>
						) : (<div onClick={() => openFileInput("banner")}></div>
                                )}
                            </div>
                            {/* 커뮤니티 메타 데이터 */}
                            <div>
                                <div>
                                    <div>
                                        {sub.imageUrl && (
                                            <Image
                                                src={sub.imageUrl}
                                                alt="커뮤니티 이미지"
                                                width={70}
                                                height={70}
                                                onClick={() => openFileInput("image")}
                                            />
                                        )}
                                    </div>
                                    <div>
                                        <div><h1>{sub.title}</h1></div>
                                          <p>{sub.name}</p>
                                    </div>
                                </div>
                            </div>
                        </div>
                        {/* 포스트와 사이드바 */}
                        <div>
                            <div>{renderPosts} </div>
                            <SideBar sub={sub} />
                        </div>
                    </>
                }
            </>
        ));
};


✔️ 커뮤니티 데이터 거져오기

api 요청 (client)

1️⃣ 비동기통신 라이브러리인 axios를 작성한 비동기함수를 fetcher에 저장.

2️⃣ router.query: 매개변수는 쿼리 매개변수로 페이지에 전송되어 subName에 저장(참고)

3️⃣ useSWR hook은 key 문자열과 fetcher 함수를 받는다.

 const fetcher = async (url: string) => { // 1️⃣ 번
    try {
      const res = await Axios.get(url);
      return res.data;
    } catch (err) {
                throw err.response.data;
    }
  };

  const router = useRouter();
  const subName = router.query.sub; // 2️⃣ 번
  const {data: sub, error} = useSWR(subName ? `subs/${subName}`: null, fetcher);// 3️⃣ 번

getSub 핸들러 (server)

1️⃣ req.params: 라우터의 매개변수

  • 예를 들어 /:id/:name 경로가 있으면 :id속성과 :name속성을 req.params.id, req.params.name으로 사용할 수 있다. www.example.com/post/1/jun 일 경우 1jun을 받는다.

참고하자! 👉 [EXPRESS] 📚 req.params / req.query / req.body 🤔 차이 정리

2️⃣ findOneByOrFail : 주어진 과 일치하는 첫 번째 엔터티를 찾는다. 일치하는 것이 없으면 반환된 약속을 거부. (참고)

const getSub = async (req: Request, res: Response) => {
  const name = req.params.name; // 1️⃣ 번
  try {
    const sub = await Sub.findOneByOrFail({ name }); // 2️⃣ 번

    return res.json(sub);
  } catch (error) {
    return res.status(404).json({ error: '게시글을 찾을 수 없습니다.' });
  }
};

router.get('/:name', userMiddleware, getSub);


✔️ Image URL 가져오기

상세페이지에 커뮤니티에 대한 데이터들이 담기는 것을 확인할 수 있는데, 이미지는 출력이 되지 않는 것을 확인할 수 있다. 서버에서 이미지 url을 전달하지 않아 null값이 출력된다.

instanceToPlain: 클래스(생성자) 개체를 일반(리터럴) 개체로 변환.

// Entity.ts
export default abstract class Entity extends BaseEntity {
	...
    
  toJSON() {
    return instanceToPlain(this);
  }
}



⭐️ Image 업로드

아래의 그림은 만약 이미지 업로드를 별도로 하지 않을 경우 출력되는 default 이미지이다. (앞서, server에서 설정해주었다.)

이제 파일을 업로드할 것인데, 위의 이미지를 클릭하였을 때 파일선택을 할 수 있도록 해보자.
즉, 아래처럼 별도의 파일선택 input을 클릭하는 것이 아니라 위의 이미지를 클릭하면 해당 기능이 동작하도록 만들어보자.



✔️ useRef 사용하여 이미지 클릭시 업로드

useRef은 특정 DOM을 가리킬 때 사용하는 Hook.

1️⃣ useRef 변수 생성

2️⃣ 실제로 동작하는 Input태그는 숨기고, ref 설정
(= 이미지를 클릭할시 Input이 클릭되는 것과 마찬가지가 된 것.)

3️⃣ 배너 이미지 업로드 (업로드했던 이미지 또는 기본 이미지를 클릭)

4️⃣ 게시글 내용 속 이미지 업로드

5️⃣ refcurrent 필드를 통해 값에 접근할 수 있는 것으로 이미지를 클릭하였다면 값이 존재하여 true가 된다. 그리고 전달된 값이 banner인지 image인지를 판별한다.

const SubPage = () => {
  const fileInputRef = React.useRef<HTMLInputElement>(null); // 1️⃣ 번

  const openFileInput = (type: string) => { // 5️⃣ 번
      const fileInput = fileInputRef.current;
      if (fileInput) {
        fileInput.name = type;
        fileInput.click();
      }
    };

	return(
    
          <input // 2️⃣  번
            type="file"
            hidden={true}
            ref={fileInputRef}
            onChange={uploadImage}
          />
			...
         // 배너 이미지 
            <div>
              {sub.bannerUrl ? (
                <div 
               	  style={backgroundImage: `url(${sub.bannerUrl})}
                  onClick={() => openFileInput('banner')}> // 3️⃣ 번
                </div>
              ) : (
                <div onClick={() => openFileInput('banner')}></div> // 3️⃣ 번
              )}
            </div>
			...
          // 커뮤니티 이미지
            {sub.imageUrl && (
                    <Image
                      src={sub.imageUrl}
                      alt="커뮤니티 이미지"
                      width={70}
                      height={70}
                      className="rounded-full"
                      onClick={() => openFileInput('image')} // 4️⃣ 번
                    />
                  )}
          
    	)
    
}


✔️ 권한에 따른 이미지 업로드

단, 자신이 생성한 커뮤니티에 대해서만 이미지를 변경 가능하게 설정해야한다.

1️⃣ 만약 ownSubfalse이면 openFileInput은 종료된다. 즉, 자신의 게시글이 아니면 이미지클릭이 불가능하다.

2️⃣ 랜더링시 게시글 데이터가 존재하지 않거나, 유저에 대한 인증이 되지 않은 경우는 그대로 종료

3️⃣ 인증조건을 만족(로그인 O)하고 게시글에 등록된 유저정보와 인증조건의 유저정보가 같다면 ownSubtrue로 변환.

  const [ownSub, setOwnSub] = useState(false);
  const { authenticated, user } = useAuthState();

  useEffect(() => {
    if (!sub || !user) return; // 2️⃣ 번
    setOwnSub(authenticated && user.username === sub.username); // 3️⃣ 번
  }, [sub]);
  
  const openFileInput = (type: string) => {
    if (!ownSub) return; // 1️⃣ 번
    ...
    }
  };


✔️ 이미지 업로드 함수 생성

이제 이미지파일을 선택하는 부분까지 완료하였고, 이미지를 선택하면 해당 이미지가 실제로 업로드 되는 것을 구현해야 한다.

api 요청 (client)

1️⃣ 만약 선택한 파일이 없다면 즉 null이라면 return

2️⃣ 배열의 첫번째 값을 file변수에 저장.

3️⃣ FormData: 이미지 같은 멀티미디어 파일을 페이지 전환 없이 폼 데이터를 비동기로 제출 하고 싶을 때나, 자바스크립트로 좀더 타이트하게 폼 데이터를 관리하고 싶을때 formData 객체를 이용한다.

4️⃣ append() 메소드로 keyvalue 값을 차례로 추가해주면, 곧 input 태그에 값을 입력하는 것과 같은 효과를 가진다.

  • formData.append(name, value) 함수를 이용해 데이터를 넣을시에 value는 문자열로만 입력 된다.

참고하자!
👉 [JS] 📚 FormData 사용법 & 응용 총정리 (+ fetch 전송하기)
👉 HTTP multipart/form-data 란?
👉 HTTP multipart/form-data 이해하기

5️⃣ api url, formData, headerpost한다.

const uploadImage = async (e: React.ChangeEvent<HTMLInputElement>) => {
    if (e.target.files === null) return; // 1️⃣ 번 
    const file = e.target.files[0]; // 2️⃣ 번 

    const formData = new FormData(); // 3️⃣ 번 
    formData.append('file', file); // 4️⃣ 번 
    formData.append('type', fileInputRef.current!.name); // 4️⃣ 번 

    try {
      await Axios.post(`/subs/${sub.name}/upload`, formData, { // 5️⃣ 번
        headers: { 'context-Type': 'multipart/form-data' },
      });
    } catch (error: any) {
      console.log(error);
    }
  };

api 작성 (server)

1️⃣ res.locals를 활용하여 user을 전역에서 사용 가능한 변수로 설정

2️⃣ findOneOrFail: 일부 ID와 일치하는 첫 번째 엔터티를 찾거나 옵션을 찾는다. 일치하는 것이 없으면 반환된 약속을 거부한다.

3️⃣ 만약 커뮤니티에 등록된 usename과 유저정보의 username이 같지 않다면 error 반환

4️⃣ res.locals를 활용하여 sub을 전역에서 사용 가능한 변수로 설정

참고하자! 👉 [JS] 📚 FormData 사용법 & 응용 총정리 (+ fetch 전송하기)

5️⃣ ownSub 다음인 upload.single(file)로 이동

const ownSub = async (req: Request, res: Response, next: NextFunction) => {
  const user: User = res.locals.user; // 1️⃣ 번

  try {
    const sub = await Sub.findOneOrFail({ where: { name: req.params.name } }); // 2️⃣ 번

    if (sub.username !== user.username) { // 3️⃣ 번
      return res
        .status(403)
        .json({ error: '사용자님의 소유 게시글이 아닙니다.' });
    }

    res.locals.sub = sub; // 4️⃣ 번

    return next(); // 5️⃣ 번
  } catch (error) {
    console.log(error);
    return res.status(500).json({ error: '문제가 발생하였습니다' });
  }
};

router.post(
  '/:name/upload',
  userMiddleware,
  authMiddleware,
  ownSub,
  upload.single('file'), // 한개의 파일만 업로드할 경우.
  uploadSubImage
);


👉 upload 함수 생성

이미지를 업로드하게 되면 프로젝트의 local에 이미지파일로 저장하는 과정이 필요할 것이다.

이때 필요한 모듈이 multer

npm install multer —save
npm i --save-dev @types/multer

참고하자!
👉 multer를 사용해 이미지 업로드하기
👉 Express Multer

upload 함수 작성

1️⃣ storage: 파일을 저장할 위치 설정

  • originalname: 사용자 컴퓨터의 파일이름
  • destination: 파일이 저장된 폴더
  • filename: 안에 있는 파일의 이름 destination

2️⃣ makeId(10): 파일마다 고유한 값 설정

3️⃣ path.extname() : 파일확장자 추출후 출력. 경로의 마지막 부분의 문자열에서 마지막 '.'에서부터 경로의 확장자를 반환한다.

4️⃣ fileFilter 함수를 만들어 png, jpeg 이미지 타입만 받겠다는 필터링 (허용되는 파일을 제어하는 기능) (MIME 타입)

const upload = multer({
  storage: multer.diskStorage({ // 1️⃣ 번
    destination: 'public/images', 
    filename: (_, file, callback) => {
      const name = makeId(10); // 2️⃣ 번
      callback(null, name + path.extname(file.originalname)); // 3️⃣ 번
    },
  }),
  fileFilter: (_, file: any, callback: FileFilterCallback) => { // 4️⃣ 번
    if (file.mimetype === 'image/jpeg' || file.mimetype === 'image/png') {
      callback(null, true);
    } else {
      callback(new Error('이미지가 아닙니다.'));
    }
  },
});

💡 makeId(10)
이미지파일의 idunique하게 만들어주기 위하여 랜덤으로 10자의 문자를 받아 name변수에 저장한다.

// utils/helper.ts
export const makeId = length => {
  let result = '';
  const characters =
    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  const charactersLength = characters.length;
  for (let i = 0; i < length; i++) {
    result += characters.charAt(Math.floor(Math.random() * charactersLength));
  }
  return result;
};

server.js에 static파일을 public 파일 안에 있고 브라우저로 접근할 때 제공을 할 수 있게 해준다.

  • 즉, static파일이 public에 존재한다고 알리는 것.
// server.js
app.use(express.static("public"));


👉 uploadFileImage 함수 생성

이제 local에 이미지가 저장되었으니 이미지를 실제 화면에 upload해야 한다.

1️⃣ res.locals를 활용하여 sub을 전역에서 사용 가능한 변수로 설정

2️⃣ client에서 요청한 데이터값을 type변수에 저장.

//client
formData.append("type", fileInputRef.current!.name);

3️⃣ 만약 typeimage, banner가 아닐시에는 유효하지 않는 파일이라고 판별 후 파일 삭제

  • 파일 path가 존재하지 않을경우 즉시 return
  • 파일 path가 존재할 경우 unlinkSync를 통해 파일 삭제 (참고)

4️⃣ 이전파일을 삭제하기 위하여 사용중인 Urn을 저장한다.

5️⃣ 새로운 파일 이름을 Urn으로 넣어준다.

6️⃣ 만약 새로운 이미지를 업로드할 경우 이전의 이미지파일은 local에 계속 남아서 쌓이게 됨으로 사용하지 않는 이미지파일은 삭제시켜준다.

  • path.resolve: 여러 인자를 넣으면 하나의 경로로 합쳐준다. (참고)
  • process.cwd(): 현재 작업 디렉토리를 반환 (참고)
  • ex) 현재작업디렉토리/public/images/업로드했던이미지파일경로
const uploadSubImage = async (req: Request, res: Response) => {
  const sub: Sub = res.locals.sub; // 1️⃣ 번
  try {
    const type = req.body.type; // 2️⃣ 번

    if (type !== 'image' && type !== 'banner') { // 3️⃣ 번
      if (!req.file?.path) {
        return res.status(400).json({ error: '유효하지 않는 파일입니다.' });
      }

      unlinkSync(req.file.path); 
      return res.status(400).json({ error: '잘못된 유형입니다.' });
    }

    let oldImageUrn = '';

    if (type === 'image') {
      oldImageUrn = sub.imageUrn || ''; // 4️⃣ 번
      sub.imageUrn = req.file?.filename || ''; // 5️⃣ 번
    } else if (type === 'banner') {
      oldImageUrn = sub.bannerUrn || ''; // 4️⃣ 번
      sub.bannerUrn = req.file?.filename || ''; // 5️⃣ 번
    }
    await sub.save();

    //  6️⃣ 번  
    if (oldImageUrn !== '') {
      const fullFilename = path.resolve(
        process.cwd(),
        'public',
        'images',
        oldImageUrn
      );
      unlinkSync(fullFilename);
    }
    return res.json(sub);
  } catch (error) {
    console.log(error);
    return res.status(500).json({ error: '문제가 발생하였습니다' });
  }
};


✔️ 이미지 업로드 확인



⭐️ Side Bar

상세페이지에 들아거면 해당 게시글에 대한 정보를 담고 있는 사이드바를 생성해보자.

파일 생성



✔️ Side Bar UI 작성

  • dayjs:JavaScript 날짜 관련 라이브러리중 가장 가벼운 라이브러리
npm install dayjs —save
const SideBar = ({ sub }: Props) => {
  const { authenticated } = useAuthState();
  return (
    <div>
      <div>
        <div>
          <p>커뮤니티에 대해서</p>
        </div>
        <div>
          <p>{sub?.description}</p>
          <div>
            <div>
              <p>100</p>
              <p>멤버</p>
            </div>
          </div>
          <p>{dayjs(sub?.createdAt).format('MM.DD.YYYY')}</p>

          {authenticated && ( // 로그인 인증시에만...
            <div>
              <Link href={`/r/${sub.name}/create`}> 포스트 생성 </Link>
            </div>
          )}
        </div>
      </div>
    </div>
  );
};


📷 Photos

profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글

관련 채용 정보