Skip to content

Commit

Permalink
Merge pull request #163 from Garodden/feature/front_article
Browse files Browse the repository at this point in the history
Feat: 아티클 프론트 연동
  • Loading branch information
lth01 authored May 14, 2024
2 parents a86b3e2 + de09705 commit 6d611f1
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 69 deletions.
111 changes: 73 additions & 38 deletions src/front/src/components/Article.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,82 +3,117 @@ import { Avatar,AvatarImage, AvatarFallback } from "./ui/avatar";
import ArticleDialog from "./ArticleDialog";
import { Link } from "react-router-dom";
import { AlertDialog, AlertDialogTrigger, AlertDialogContent,AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from "./ui/alert-dialog";
import { GenerateLiElUUID } from "@/utils/common";
import { FullDateFormatString, GenerateLiElUUID } from "@/utils/common";
import CommentCircleIcon from "./svg/CommentCircleIcon";
import { Comment } from "./Comment";
import { useEffect, useState } from "react";
import { MessageCircleIcon, MessageCircleOffIcon } from "lucide-react";
import { getArticlePictures } from "@/utils/Items";
import { getAccessTokenInfo } from "@/utils/Cookie";
import { deleteArticle } from "@/utils/API";

const Article = ({id, content, createdAt, updatedAt, images, user}) =>{
const Article = ({id, contents, createdAt, updatedAt, images, user, afterDeleteFn}) =>{
const [commentVisibility, setCommentVisibility] = useState(false);

//유저 프로필 이미지
const [profilePic, setProfilePic] = useState("");

//아티클 첨부 이미지들
const [articlePicItems, setArticlePicItems] = useState([]);

//jwt Token의 유저 정보와 일치하는지 확인하는 메서드
const [articleOwner, setArticleOwner] = useState(false);


useEffect(() =>{
//유저 프로필 이미지가 있다면
if(user.image){
setProfilePic(`data:image/png;base64,${user.image}`);
}

//content설정

//아티클 첨부 이미지 업데이트
if(images){
setArticlePicItems(getArticlePictures(images));
}

isWirter();
},[]);

const toggleCommentVisibility = () =>{
setCommentVisibility(!commentVisibility);
}

const isWirter = () =>{
const {sub} = getAccessTokenInfo();
if(sub === user.email){
setArticleOwner(true)
}
}

const doDelete = () =>{
deleteArticle(id)
.then(() =>{
alert("삭제가 완료되었습니다.");
afterDeleteFn(id);
})
.catch(e => console.log(e))
}

return (
<Card key={GenerateLiElUUID()}>
<Card key={id}>
<CardHeader className="flex-row justify-between">
<div className="flex items-center gap-4">
<Avatar className="w-20 h-20">
<AvatarImage alt="유저 프로필이미지" src={}/>
<AvatarImage alt="유저 프로필이미지" src={profilePic}/>
<AvatarFallback></AvatarFallback>
</Avatar>
<div className="flex flex-col justify-center">
<h2 className="font-bold text-lg">username</h2>
<span className="text-black/50 ">id</span>
<h2 className="font-bold text-lg">{user.realName}</h2>
<span className="text-black/50 ">{user.email}</span>
</div>
</div>
<div className="p-2">
<div className="modify-zone flex justify-end gap-4">
<ArticleDialog>
<Link>edit</Link>
</ArticleDialog>
{/* delete */}
<AlertDialog>
<AlertDialogTrigger asChild>
<Link>delete</Link>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
정말 게시글을 삭제하시겠어요?
</AlertDialogTitle>
<AlertDialogDescription>
게시글을 삭제하면 되돌릴 수 없어요. 신중하게 선택해주세요. 정말 삭제하시겠어요?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>취소</AlertDialogCancel>
<AlertDialogAction onClick={() =>{alert("삭제 api호출")}}>삭제</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{
articleOwner ?
<>
<ArticleDialog {...{id, contents, user}}>
<Link>edit</Link>
</ArticleDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<Link>delete</Link>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
정말 게시글을 삭제하시겠어요?
</AlertDialogTitle>
<AlertDialogDescription>
게시글을 삭제하면 되돌릴 수 없어요. 신중하게 선택해주세요. 정말 삭제하시겠어요?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>취소</AlertDialogCancel>
<AlertDialogAction onClick={doDelete}>삭제</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</> :
<></>
}
</div>
<span className="mt-2">2024.04.29 15:00</span>
<span className="mt-2">
{FullDateFormatString(new Date(updatedAt))}
</span>
</div>
</CardHeader>
<CardContent className="p-8">
<span>
{content}
{contents}
</span>
<ul className="mt-4 list-none grid grid-cols-3 justify-items-center gap-4">
<li className="w-[200px] h-[200px] bg-slate-400"></li>
<li className="w-[200px] h-[200px] bg-slate-400"></li>
<li className="w-[200px] h-[200px] bg-slate-400"></li>
<li className="w-[200px] h-[200px] bg-slate-400"></li>
<li className="w-[200px] h-[200px] bg-slate-400"></li>
{articlePicItems}
</ul>
</CardContent>
<CardFooter className="border-t p-3 pl-4 flex gap-4 items-center">
Expand Down
82 changes: 65 additions & 17 deletions src/front/src/components/ArticleDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,88 @@ import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar";
import { Textarea } from "@/components/ui/textarea";
import FileInput from "@/components/FileInput";
import { Button } from "@/components/ui/button";
import { useState, useEffect } from "react";
import { createArticle, fetchLoginUserProfile, updateArticle } from "@/utils/API";
import { createArticleReqParam, updateArticleReqParam } from "@/utils/Parameter";
import { getAccessTokenInfo } from "@/utils/Cookie";

const ArticleDialog = (props) =>{
const resource = {
btnText: "생성",
clickCallback: () =>{
const file = [...document.getElementById('picture').files];
const [open, setOpen] = useState(false);

console.log(file);
alert("save!")
},
initFn: () => {}
//여기서 유저정보를 어떻게 ..?
const {id, user, contents} = props;
const [userInfo, setUserInfo] = useState(null);
const [profilePic, setProfilePic] = useState("");
//textArea
const [textContents, setTextContents] = useState("");
const [files, setFiles] = useState([]);
const [mode, setMode] = useState("create");

useEffect(() =>{
setTextContents("");
setFiles([]);
setMode("create");
setUserInfo(null);
setProfilePic("");
}, [open]);

const fetchUserInfo = (dialogIsOpen) =>{
fetchLoginUserProfile()
.then(async (userInfo) =>{
setUserInfo(userInfo);

if(id){
handleEditMode();
}

setProfilePic(`data:image/png;base64,${userInfo.image}`);
});
}

const doArticleCreate = () =>{
const reqParam = createArticleReqParam(textContents, files);

createArticle(reqParam)
.then((data) => {
setOpen(false);
})
}

const doArticleUpdate = () =>{
const reqParam = updateArticleReqParam(textContents, files);

updateArticle(id, reqParam)
.then((data) =>{
console.log(data);
setOpen(false);
})
}

const handleEditMode = () =>{
setMode("edit");
setTextContents(contents);
}

return (
<Dialog>
<Dialog open={open} onOpenChange={(isOpen) =>{setOpen(isOpen); fetchUserInfo();}}>
<DialogTrigger asChild {...props}></DialogTrigger>
<DialogContent onPointerDownOutside={ev => ev.preventDefault()}>
<DialogHeader>
<div className="flex items-center gap-4">
<Avatar className="w-8 h-8">
<AvatarImage alt="@shadcn" src="/placeholder-avatar.jpg" />
<AvatarFallback>hi</AvatarFallback>
<Avatar className="w-12 h-12">
<AvatarImage alt="유저 프로필 이미지" src={profilePic} />
<AvatarFallback></AvatarFallback>
</Avatar>
<div className="flex flex-col justify-center">
<h2 className="font-bold text-lg">username</h2>
<span className="text-black/50 ">id</span>
<h2 className="font-bold text-lg">{userInfo?.realName}</h2>
<span className="text-black/50 ">{userInfo?.email}</span>
</div>
</div>
</DialogHeader>
<Textarea className="mt-4 h-48 resize-none" placeholder="input your content"></Textarea>
<FileInput></FileInput>
<Textarea onChange={ev => {setTextContents(ev.target.value)}} value={textContents} className="mt-4 h-48 resize-none" placeholder="input your content"></Textarea>
<FileInput onChange={setFiles}></FileInput>
<DialogFooter>
<Button className="bg-[#6866EB]" onClick={resource.clickCallback} type="submit">{resource.btnText}</Button>
<Button className="bg-[#6866EB]" onClick={mode === 'create' ? doArticleCreate : doArticleUpdate} type="submit">{mode === 'create' ? '생성' : '수정'}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
Expand Down
4 changes: 2 additions & 2 deletions src/front/src/components/FileInput.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

const FileInput = () =>{
const FileInput = ({onChange}) =>{
return (
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="picture">첨부파일(shift, control키로 여러 파일 선택 가능)</Label>
<Input id="picture" accept=".png" multiple type="file"/>
<Input id="picture" onChange={(ev) => onChange([...ev.target.files])} accept=".png" multiple type="file"/>
</div>
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/front/src/components/Layout/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { logout } from "@/utils/API";

const Header = (props) =>{
return (
<header className={`flex items-center border-b bg-white dark:border-gray-800 overflow-hidden ${props.className}`}>
<header className={`flex items-center border-b bg-white dark:border-gray-800 overflow-hidden z-10 ${props.className}`}>
<div className="flex items-center px-4 py-2 justify-between overflow-hidden w-full">
<div className="flex items-center gap-2 min-w-[500px]">
<Link to="/">
Expand Down
24 changes: 19 additions & 5 deletions src/front/src/routes/MainFeed.jsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,31 @@
import Article from "@/components/Article";
import Header from "@/components/Layout/Header";
import { readMainFeedArticles } from "@/utils/API";
import { getArticleItems } from "@/utils/Items";
import { useEffect, useState } from "react";

const MainFeed = () =>{
const [articles, setArticles] = useState([]);

useEffect(() =>{
readMainFeedArticles()
.then((articles) =>{
setArticles(articles);
});
}, []);

const afterArticleDelete = (articleId) =>{
setArticles(articles.filter(item => item.id !== articleId));
}

return (
<main className="h-screen">
<Header className="fixed w-full"></Header>
<section className="article-feed pt-[61px] px-[20%]">
<div className="py-4 overflow-auto grid gap-4 justify-center ">
<Article></Article>
<Article></Article>
<Article></Article>
<Article></Article>
<Article></Article>
{
getArticleItems(articles, afterArticleDelete)
}
</div>
</section>
</main>
Expand Down
7 changes: 5 additions & 2 deletions src/front/src/routes/Mypage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,11 @@ const MyPage = () => {
<div className="w-3/5 p-5 bg-white shadow-lg rounded mt-6 overflow-hidden">
<Avatar className="w-40 h-40">
{profilePic ? (
<AvatarImage src={profilePic} alt="User profile picture"/>
) : (
<>
<AvatarImage src={profilePic} alt="User profile picture"/>
<AvatarFallback>{username.charAt(0)}</AvatarFallback>
</>
) : (
<AvatarFallback>{username.charAt(0)}</AvatarFallback>
)}
</Avatar>
Expand Down
8 changes: 8 additions & 0 deletions src/front/src/routes/Test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import UserPage from "./UserPage";
import { createArticle, deleteArticle, fetchLoginUserProfile, readAllMyArticle, readMainFeedArticles, saveProfile, updateArticle, withdraw } from "@/utils/API";
import { createArticleReqParam, saveProfileReqParam, updateArticleReqParam, withdrawReqParam } from "@/utils/Parameter";
import { getAccessToken, getAccessTokenInfo } from "@/utils/Cookie";
import { getArticleItems } from "@/utils/Items";


const Test = () => {
Expand All @@ -29,6 +30,7 @@ const Test = () => {

/**아티클 API 테스트 */
const [articles, setArticles] = useState([]);
const [articleItems, setArticleItems] = useState(<></>);
const [articleFiles, setArticleFiles] = useState([]);

const resource = {
Expand Down Expand Up @@ -73,6 +75,7 @@ const Test = () => {
readAllMyArticle()
.then((response) => {
setArticles([...response]);
setArticleItems(getArticleItems([...response]));
console.log(response);
}
);
Expand Down Expand Up @@ -133,6 +136,11 @@ const Test = () => {

<Input type="file" accept=".png" multiple onChange={ev => {setArticleFiles([...ev.target.files] || []);}}></Input>
</div>
<div>
{
articleItems
}
</div>
</main>

);
Expand Down
2 changes: 1 addition & 1 deletion src/front/src/utils/Cookie.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,5 +65,5 @@ export const removeRefreshToken = () =>{
export const getAccessTokenInfo = () =>{
const token = getAccessToken().split(" ")[1];
const payload = token.substring(token.indexOf('.') + 1, token.lastIndexOf('.'));
return base64.decode(payload);
return JSON.parse(base64.decode(payload));
}
Loading

0 comments on commit 6d611f1

Please sign in to comment.