본문 바로가기

Java/Spring

[Spring] 간단한 SNS 만들기 #6

What to do?

해쉬태그 기능개발하기

구현한 사이트 모습


Entity, Dto  Hashtag 필드 추가하기

 

  • PostEntity.java
    • Hashtags 컬럼을 String 타입으로 할지 Set<String> 타입으로 할지 고민했다.
    • 생각해보니 해쉬태그는 정확히 일치하는 경우만 조회하고 싶다(Exact Match)라고 생각한다면 Set<String>이 맞는 것 같았다
@Setter
@Getter
@Entity
@Table(name = "\"post\"")
@SQLDelete(sql = "UPDATE \"post\" SET removed_at = NOW() WHERE id=?")
@Where(clause = "removed_at is NULL")
public class PostEntity extends AuditingFields {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "title", nullable = false)
    private String title;
    @Column(name = "content", columnDefinition = "TEXT", nullable = false)
    private String content;
    @ElementCollection(fetch = FetchType.LAZY)
    private Set<String> hashtags = new HashSet<String>();
    @ManyToOne @JoinColumn(name = "user_id")
    private UserEntity user;
    @OneToMany(fetch = FetchType.LAZY) @JoinColumn(name = "post_id")
    private List<CommentEntity> comments;

    private PostEntity(String title, String content, UserEntity user, Set<String> hashtags, List<CommentEntity> comments) {
        this.title = title;
        this.content = content;
        this.hashtags = hashtags;
        this.user = user;
        this.comments = comments;
    }

    protected PostEntity(){}

    public static PostEntity of(String title, String content, UserEntity user, Set<String> hashtags) {
        return new PostEntity(title, content, user, hashtags, List.of());
    }

    public static PostDto dto(PostEntity entity){
        return PostDto.of(
                entity.getId(),
                entity.getTitle(),
                entity.getContent(),
                entity.getUser().getNickname(),
                entity.getHashtags(),
                entity.getCreatedAt(),
                entity.getModifiedAt(),
                entity.getCreatedBy(),
                entity.getModifiedBy()
        );
    }
}

 

  • PostDto
@Getter
@Setter
public class PostDto {
    private Long id;
    private String title;
    private String content;
    private String nickname;
    private Set<String> hashtags;
    private LocalDateTime createdAt;
    private LocalDateTime modifiedAt;
    private String createdBy;
    private String modifiedBy;

    private PostDto(
            Long id,
            String title,
            String content,
            String nickname,
            Set<String> hashtags,
            LocalDateTime createdAt,
            LocalDateTime modifiedAt,
            String createdBy,
            String modifiedBy
    ) {
        this.id = id;
        this.title = title;
        this.content = content;
        this.nickname = nickname;
        this.hashtags = hashtags;
        this.createdAt = createdAt;
        this.modifiedAt = modifiedAt;
        this.createdBy = createdBy;
        this.modifiedBy = modifiedBy;
    }

    protected PostDto(){}

    public static PostDto of(Long id, String title, String content, String nickname, Set<String> hashtags, LocalDateTime createdAt, LocalDateTime modifiedAt, String createdBy, String modifiedBy) {
        return new PostDto(id, title, content, nickname, hashtags, createdAt, modifiedAt, createdBy, modifiedBy);
    }
}

Repository  Hashtag로 조회기능 추가하기

 

  • PostRepository.java
    • findByHashtags 메써드 추가

 

@Repository
public interface PostRepository extends JpaRepository<PostEntity, Long> {
    Page<PostEntity> findAll(Pageable pageable);
    Page<PostEntity> findAllByUser(Pageable pageable, UserEntity userEntity);
    Page<PostEntity> findByHashtags(Pageable pageable, String hashtag);
    @Modifying
    @Query(value = "UPDATE PostEntity entity SET removed_at = NOW() where entity.id = :postId", nativeQuery = true)
    void deleteByPostId(@Param("postId") Long postId);
}

Util 해쉬태그 문자열 처리

 

  • Front-end : #로 연결된 String을 보냄
  • Back-end
    • 정규표현식을 사용해 Set<String>으로 Parsing
    • \w : _ , 영어, 숫자
    • ㄱ-힣 : 초성을 포함한 한글 (초성만 있는게 싫다면 가-힣으로 변경)
@Configuration
public class HashtagParser {
    /**
     * 해쉬태크 문자열을 set으로 만들어줌
     * Ex) "#사과 #파인애플" → Set.of("사과", "파인애플")
     * @param hashtags : 해쉬태그 String
     * @return 해쉬태그 Set
     */
    public Set<String> stringToSet(String hashtags) {
        if (hashtags == null) {
            return Set.of();
        }
        // #로 시작하고, 한글(가-힣), _ , 영어나 숫자 (\w)만
        Pattern pattern = Pattern.compile("#[\\wㄱ-힣]+");
        // 공백제거
        Matcher matcher = pattern.matcher(hashtags.strip());
        Set<String> result = new HashSet<>();
        while (matcher.find()) {
            result.add(matcher.group().replace("#", ""));
        }
        return result;
    }
}

Service  포스팅 생성/수정해쉬태그 컬럼 추가 & 해쉬태그로 조회 기능 추가하기

 

  • 포스팅 작성, 수정 : 해쉬태그 컬럼 추가
  • 해쉬태그로 포스팅 조회 메써드 추가
@Slf4j
@Service
@RequiredArgsConstructor
public class PostService {
    private final UserRepository userRepository;
    private final PostRepository postRepository;
    private final CommentRepository commentRepository;
    private final LikeRepository likeRepository;
    private final NotificationRepository notificationRepository;
    private final NotificationService notificationService;
    private final HashtagParser hashtagParser;

    ...    
    /**
     * 포스트 작성요청
     * @param title : 제목
     * @param content : 본문
     * @param user : 로그인한 유저
     * @param hashtags : 해쉬태그       
     * @return 저장된 post id
     */
    @Transactional
    public Long createPost(String title, String content, UserEntity user, String hashtags){
        return postRepository.save(PostEntity.of(title, content, user, hashtagParser.stringToSet(hashtags))).getId();
    }

    /**
     * 포스팅 조회 by 해쉬태그
     * @param pageable
     * @param hashtag 조회할 해쉬태그
     * @return PostDto 페이지
     */
    @Transactional(readOnly = true)
    public Page<PostDto> getPostsByHashtag(Pageable pageable, String hashtag){
        return postRepository.findByHashtags(pageable, hashtag).map(PostEntity::dto);
    }

    /**
     * 포스트 수정요청
     * @param postId 수정요청한 포스트 id
     * @param title 제목
     * @param content 본문
     * @param user 로그인한 유저
     * @return 저장된 포스트 id
     */
    @Transactional
    public void modifyPost(Long postId, String title, String content, UserEntity user, String hashtags){
        PostEntity post = findByPostIdOrElseThrow(postId);
        if (!post.getUser().getUsername().equals(user.getUsername())){
            // 포스트 작성자와 수정 요청한 사람이 일치하는지 확인
            throw CustomException.of(CustomErrorCode.NOT_GRANTED_ACCESS);
        }
        post.setTitle(title);
        post.setContent(content);
        post.setHashtags(hashtagParser.stringToSet(hashtags));
        postRepository.save(post);
    }

   ...
}

Controller  포스팅 생성/수정 해쉬태그 컬럼 추가 & 해쉬태그로 조회 기능 추가하기

 

  • 포스팅 작성, 수정 : 해쉬태그 컬럼 추가
  • 해쉬태그로 포스팅 조회 메써드 추가
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1")
public class PostController {
    private final PostService postService;
    
    ...

    // 특정 해쉬태그로 조회
    @GetMapping("/post/hashtag")
    public CustomResponse<Page<GetPostResponse>> getPostsByHashtag(
            @RequestParam("hashtag") String hashtag,
            @PageableDefault(size = 20, sort = "createdAt",direction = Sort.Direction.DESC) Pageable pageable
    ){
        return CustomResponse.success(postService.getPostsByHashtag(pageable, hashtag).map(GetPostResponse::from));
    }

    // 포스팅 작성
    @PostMapping("/post")
    public CustomResponse<Long> createPost(@RequestBody CreatePostRequest req, Authentication authentication){
        UserEntity user = (UserEntity) authentication.getPrincipal();
        return CustomResponse.success(postService.createPost(req.getTitle(), req.getContent(), user, req.getHashtags()));
    }

    // 포스팅 수정
    @PutMapping("/post/{postId}")
    public CustomResponse<Void> modifyPost(@PathVariable Long postId, @RequestBody ModifyPostRequest req, Authentication authentication){
        UserEntity user = (UserEntity) authentication.getPrincipal();
        postService.modifyPost(postId, req.getTitle(), req.getContent(), user, req.getHashtags());
        return CustomResponse.success();
    }

   ...
}

Front-End (React 코드)

 

  • 홈화면

홈화면

import { Container } from "@mui/system";

const { Typography, Tooltip } = require("@mui/material")

const Home=()=>{

    const githubRepositoryLink = "https://github.com/nsm4421/Prj";

    return(
        <>
        <Container>
            <Typography variant="h5" sx={{marginTop:"10vh"}}>
                커뮤니티 사이트 만들기
            </Typography>

            <Typography paragraph sx={{marginTop:"5vh"}}>
                <Typography fontSize={'large'}>Front-End</Typography>
                <Typography fontSize={'small'}>React</Typography>
            </Typography>

            <Typography paragraph sx={{marginTop:"5vh"}}>
                <Typography fontSize={'large'}>Back-End</Typography>
                <Typography fontSize={'small'}>Spring Boot</Typography>
            </Typography>

            <Typography paragraph sx={{marginTop:"5vh"}}>
                <Typography fontSize={'large'}>Github Repository</Typography>
                <Tooltip title="move">
                    <Typography sx={{display:"inline"}} fontSize={'small'}>
                        <a href={githubRepositoryLink}>
                            {githubRepositoryLink}
                        </a>
                    </Typography>
                </Tooltip>
            </Typography>
        </Container>
        </>
    )
}

export default Home;

  • 포스팅 카드
    • 오른쪽 아래 해쉬태그 목록을 보여줌

포스팅 조회 뷰

const PostCard = ({post}) => {
    const [openDialog, setOpenDialog] = useState(false);
    const handleDialog = (e) => {
        e.preventDefault();
        setOpenDialog(!openDialog);
    }

    return (
        <Card sx={{ minWidth: 275 }}>

            {/* 다이얼로그 창 (포스팅 세부정보) */}
            <FullScreenDialog open={openDialog} setOpen={setOpenDialog} post={post}/>
            
            <CardContent onClick={handleDialog}>
                <CardActions sx={{justifyContent:"space-between"}}>
                    {/* 제목 */}
                    <Typography variant="h5" component="strong" color="primary">{post.title}</Typography>
                    {/* 닉네임(작성자) */}
                    <Typography variant="span" component="span" color="text.secondary">
                        {post.nickname}
                    </Typography>
                </CardActions>

                <CardActions sx={{justifyContent:"space-between"}}>
                    {/* 본문 - 100글자까지만 보여주고 ... 붙이기 */}
                    <Box sx={{padding:'1vh'}}>
                        <Typography variant="body2">{post.content.length>=50?post.content.slice(0,50)+" ...":post.content}</Typography>
                    </Box>
                    {/* 닉네임(작성자) */}
                    <Typography variant="span" component="span" color="text.secondary">
                        {post.createdAt}
                    </Typography>
                </CardActions>
                {/* 해쉬태그 */}
                <Box sx={{float:'right'}}>
                    {post.hashtags.map((h, i)=>{
                        return(
                            <Typography variant="span" component="span" color="primary" key={i} sx={{marginRight:"2vh"}}>
                                #{h}
                            </Typography>
                        )
                    })}
                </Box>
            </CardContent>
        </Card>
    );
}

export default PostCard;

  • 포스팅 작성 뷰
    • 해쉬태그 작성 기능 추가
      • 한글,영어,숫자만 사용해 10자 내외로 작성가능

해쉬태그 입력창
해쉬태그 입력

import axios from "axios";
import { useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import CreateIcon from '@mui/icons-material/Create';
import { Button, FormControl, IconButton, InputAdornment, InputLabel, Modal, OutlinedInput, TextField, Tooltip, Typography } from "@mui/material";
import { Box, Container } from "@mui/system";
import DynamicFeedIcon from '@mui/icons-material/DynamicFeed';
import UploadIcon from '@mui/icons-material/Upload';
import AddCircleOutlineRoundedIcon from '@mui/icons-material/AddCircleOutlineRounded';
import RemoveCircleOutlineRoundedIcon from '@mui/icons-material/RemoveCircleOutlineRounded';
import DetailPost from "./DetailPost";

const WritePost = () => {
    const MAX_HASHTAG_NUM = 5;
    const MAX_HASHTAG_LENGTH = 10;
    const endPoint = "/api/v1/post";
    const navigator = useNavigate();
    // ------ state ------
    const [title, setTitle] = useState("");
    const [content, setContent] = useState("");
    const [hashtags, setHashtags] = useState(Array(MAX_HASHTAG_NUM).fill(""));
    const [numHashtags, setNumHashtags] = useState(0);
    const [isLoading, setIsLoading] = useState(false);

    // ------ handler ------
    const handleTitle = (e) =>{
        setTitle(e.target.value.slice(0, 100));
    }
    const handleContent = (e) => {
        setContent(e.target.value.slice(0, 2000));
    }
    const handleHashtag = (i) => (e) => {
        let newHashtag = [...hashtags]
        if (e.target.value === ""){
            newHashtag[i] = ""
        } else {
            const matched = e.target.value.match(/^[\wㄱ-힣]+$/g);                            // 한글,숫자,영어
            newHashtag[i] = matched?matched[0].slice(0, MAX_HASHTAG_LENGTH):hashtags[i];     // 최대 10자
        }
        setHashtags(newHashtag);
    }
    const addHashtag = () => {
        setNumHashtags(Math.min(MAX_HASHTAG_NUM, numHashtags+1));
    }
    const deleteHashtag = () => {
        setNumHashtags(Math.max(0, numHashtags-1));
    }
    const handleSubmit = async (e) =>{
        e.preventDefault();
        if (!title){
            alert("제목을 입력해주세요");
            return;
        }
        if (!content){
            alert("본문을 입력해주세요");
            return;
        }
        setIsLoading(true);
        // Hashtags 변수를 문자열로 변경
        // Ex) ["사과","파인애플","","",""] → "#사과#파인애플"
        let hashtagString = hashtags.map((h, i)=>{
            const matched = h.match(/^[\wㄱ-힣]+$/g);  
            return matched?matched[0]:null;
        }).filter((v, _)=>{
            return v;
        }).join("#");
        hashtagString = handleHashtag?"#"+hashtagString:""
        console.log(hashtagString);
        await axios.post(
            endPoint,
            {title, content, hashtags:hashtagString},
            {
                headers:{
                    Authorization : localStorage.getItem("token")
                }
            }
        ).then((res)=>{
            navigator("/post");
        }).catch((err)=>{
            alert("포스팅 업로드에 실패하였습니다 \n" + (err.response.data.resultCode??"알수 없는 서버 오류"));
            console.log(err);
        }).finally(()=>{
            setIsLoading(false);
        });
    }
    
    return (
        <>
        <Container>
            
            {/* 머릿글 */}
            <Modal> 
                <DetailPost/>
            </Modal>

            <Box sx={{marginTop:'5vh', display:'flex', justifyContent:'space-between', alignContent:'center'}}>
                <Typography variant="h5" component="h5">
                    <CreateIcon/> 포스팅 작성하기
                </Typography>
                <Box>
                    <Link to="/post">
                        <Button variant="contained" color="success" sx={{marginRight:'10px'}}>
                            <DynamicFeedIcon sx={{marginRight:'10px'}}/>포스팅 페이지로
                        </Button>
                    </Link>
                    <Button variant="contained" color="error" type="submit" onClick={handleSubmit} disabled={isLoading}>
                        <UploadIcon sx={{marginRight:'10px'}}/>제출
                    </Button>
                </Box>
            </Box>

            {/* 제목작성 */}
            <Box sx={{marginTop:'5vh'}}>
            <Box sx={{alignItems:'center'}}>
                    <Typography variant="h6" component="h6" sx={{display:'inline', marginRight:'20px'}}>제목</Typography>
                    <Typography variant="span" component="span" sx={{color:'gray'}}>
                        ({title.length} / 100)
                    </Typography>
                </Box>
                <TextField
                    sx={{width:'100%'}}
                    onChange={handleTitle}
                    variant="outlined"
                    color="warning"
                    maxRows={1}
                    value={title}
                    focused
                />  
            </Box>
            
            {/* 본문작성 */}
            <Box sx={{marginTop:'5vh'}}>
                <Box sx={{alignItems:'center'}}>
                    <Typography variant="h6" component="h6" sx={{display:'inline', marginRight:'20px'}}>본문</Typography>
                    <Typography variant="span" component="span" sx={{color:'gray'}}>
                        ({content.length} / 2000)
                    </Typography>
                </Box>
                <TextField
                    sx={{width:'100%'}}
                    onChange={handleContent}
                    variant="outlined"
                    color="warning"
                    value={content}
                    multiline
                    focused
                />
            </Box>

             {/* 해쉬태그작성 */}
             <Box sx={{marginTop:'5vh'}}>
                <Box sx={{alignItems:'center'}}>
                    <Typography variant="h6" component="h6" sx={{display:'inline', marginRight:'20px'}}>해쉬태그</Typography>
                    <Typography variant="span" component="span" sx={{color:'gray'}}>
                        ({numHashtags} / 5)
                    </Typography>
                    {/* 해쉬태그 추가/삭제히기 */}
                    <IconButton sx={{height:'100%'}}>
                        <AddCircleOutlineRoundedIcon onClick={addHashtag}/>
                    </IconButton>
                    <IconButton sx={{height:'100%'}}>
                        <RemoveCircleOutlineRoundedIcon onClick={deleteHashtag}/>
                    </IconButton>
                </Box>
                {/* 해쉬태그 입력창 */}
                {
                   hashtags.map((h, i)=>{
                        if (numHashtags>i){
                            return (
                                <Tooltip title={`한글,영어,숫자로 ${MAX_HASHTAG_LENGTH}자이내(띄어쓰기,특수문자x)`}>
                                    <FormControl sx={{ m: 1 }} onChange={handleHashtag(i)}>
                                        <OutlinedInput value={h} startAdornment={<InputAdornment position="start">#</InputAdornment>} />
                                    </FormControl>
                                </Tooltip>
                            )
                        }
                        return;
                   })
                }
                
            </Box>           

        </Container>
        </>
    )
}

export default WritePost;

  • 포스팅 단건조회 뷰

포스팅 단건조회 뷰

import { styled } from '@mui/material/styles';
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions';
import Collapse from '@mui/material/Collapse';
import Avatar from '@mui/material/Avatar';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import { red } from '@mui/material/colors';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import SendIcon from '@mui/icons-material/Send';
import ThumbUpIcon from '@mui/icons-material/ThumbUp';
import ThumbDownIcon from '@mui/icons-material/ThumbDown';
import { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { Container } from '@mui/system';
import axios from 'axios';
import { userState } from '../../recoil/user';
import { useRecoilState } from 'recoil';
import { Box, Button, DialogContent, Grid, Pagination, TextField, Tooltip } from '@mui/material';
import FullScreenDialog from './FullScreenDialog';
import ListItemText from '@mui/material/ListItemText';
import ListItem from '@mui/material/ListItem';
import List from '@mui/material/List';
import Divider from '@mui/material/Divider';

const ExpandMore = styled((props) => {
  const { expand, ...other } = props;
  return <IconButton {...other} />;
})(({ theme, expand }) => ({
  transform: !expand ? 'rotate(0deg)' : 'rotate(180deg)',
  marginLeft: 'auto',
  transition: theme.transitions.create('transform', {
    duration: theme.transitions.duration.shortest,
  }),
}));

const DetailPost = ({postId}) => {

    // ------- States  -------         
    const [user, setUser] = useRecoilState(userState);
    // 포스팅
    const [title, setTitle] = useState("");
    const [content, setContent] = useState("");
    const [author, setAuthor] = useState("");
    const [createdAt, setCreatedAt] = useState("");
    const [hashtags, setHashtags] = useState([""]);
    // 좋아요 & 싫어요
    const [emotion, setEmotion] = useState("");
    const [likeCount, setLikeCount] = useState({LIKE:0, DISLIKE:0});
    // 댓글
    const [comments, setComments] = useState([]);
    const [userComment, setUserComment] = useState("");
    const [currentPage, setCurrentPage] = useState(0);
    const [totalPage, setTotalPage] = useState(1);
    // 기타
    const [expanded, setExpanded] = useState(false);
    const [isLoading, setIsLoading] = useState(false);
    
    // ------- hooks  -------  
    useEffect(()=>{                                         // 포스팅 정보 가져오기
        getPostRequest().then((res)=>{
            setTitle(res.title);
            setContent(res.content);
            setAuthor(res.nickname);
            setCreatedAt(res.createdAt);
            setHashtags(res.hashtags);
        })
    }, [])

    useEffect(()=>{                                         // 댓글창 열기 / 닫기
        if (expanded){
            getCommentRequest().then((res)=>{
                setComments([...res.content]);              // 댓글
                setCurrentPage(res.pageable.pageNumber);    // 페이지
                setTotalPage(res.totalPages);               // 전체페이지
            });
        }
    }, [expanded, isLoading])

    useEffect(()=>{                                     // 댓글 페이지 넘기기
        getCommentRequest().then((res)=>{
            setComments([...res.content]);              // 댓글
            setCurrentPage(res.pageable.pageNumber);    // 페이지
            setTotalPage(res.totalPages);               // 전체페이지
        });
    }, [currentPage])

    // ------- Rest API  ------- 

    // 포스팅 가져오기
    const getPostRequest = async () => {
        const endPoint = `/api/v1/post/detail?pid=${postId}`;
        return await axios
            .get(endPoint, {
                headers:{
                    Authorization:user.token??localStorage.getItem("token")
                }
            }).then((res)=>{
                return res.data.result
            }).then((res)=>{
                return res;
            }).catch((err)=>{
                console.log("getPostRequest : ", err);
        });
    }

    // 댓글 가져오기
    const getCommentRequest = async () => {
        const endPoint = `/api/v1/comment?pid=${postId}&page=${currentPage}`
        return await axios
            .get(endPoint, {
                headers:{
                    Authorization:user.token??localStorage.getItem("token")
                }
            }).then((res)=>{
                return res.data.result;                
            }).catch((err)=>{
                console.log(err);
            });
    }

    // TODO : 좋아요 & 싫어요 개수
    const getLikeCountRequest = async () => {
        // const endPoint = `/api/v1/like/pid=${postId}`;
        // return await axios.get(endPoint, {
        //     headers:{
        //         Authorization:user.token??localStorage.getItem("token")
        //     }
    }   

    // 댓글작성 요청
    const submitCommentRequest = async () => {
        const endPoint = `/api/v1/comment`
        const data = {postId, content:userComment}
        await axios.post(endPoint, data, {
            headers:{Authorization:user.token??localStorage.getItem("token")}})
    }

    // TODO : 좋아요 & 싫어요 요청
    const sendLikeRequest = async (likeType) => {
        const endPoint = "/api/v1/like";
        const data = {postId, likeType}
        await axios.post(endPoint, data, {
            headers:{Authorization:user.token??localStorage.getItem("token")}
        });
    }   

    // ------- handeler  -------

    // 댓글창 열기 / 닫기
    const handleExpandClick = () => {
        setExpanded(!expanded);
    };

    // 댓글 최대 500자
    const handleUserComment = (e) => {
        setUserComment(e.target.value.slice(0, 500));
    }

    // 댓글 입력
    const handleSumbitComment = (e) => {
        setIsLoading(true);
        submitCommentRequest().then(()=>{
            // 댓글 작성 성공시 댓글 다시 불러오기
            getCommentRequest().then((res)=>{
                setComments([...res.content]);              // 댓글
                setCurrentPage(res.pageable.pageNumber);    // 페이지
                setTotalPage(res.totalPages);               // 전체페이지
            });
            setUserComment("");
        }).catch((err)=>{
            console.log(err);
        })
        setIsLoading(false);
    }

    const handleLike = () => {
        // TODO 
    }

    const handleCommentPage = (e) => {
        setCurrentPage((parseInt(e.target.outerText)??1)-1);
    }

    return (
        <Card>
            {/* 작성자 & 작성시간 */}
            <CardHeader
                avatar={
                    <Avatar sx={{ bgcolor: red[500] }} aria-label="recipe">
                        R
                    </Avatar>
                }
                title={author}
                subheader={createdAt}
            />

            {/* TODO : 이미지 삽입기능 */}
            {/* <CardMedia
                component="img"
                height="194"
                image="/static/images/cards/paella.jpg"
                alt="Paella dish"
            /> */}

            {/* 본문 */}
            <CardContent>
                <TextField value={content} sx={{width:"100%"}} multiline variant='filled'/>
                <Divider />
            </CardContent>

            {/* 해쉬태그 */}           
            <Box sx={{display:'flex', width:'100%', padding:'1vh'}} color="primary">
                {hashtags.map((h, i)=>{
                    return(
                        <Typography variant="span" component="span" color="primary" key={i} sx={{marginLeft:"2vh"}}>
                            #{h}
                        </Typography>
                    )
                })}           
                <Divider />   
            </Box>
          
            <CardActions disableSpacing>    
                {/* TODO : 좋아요/싫어요 기능 */}
                {/* 좋아요 아이콘 */}
                <IconButton onClick={handleLike}>
                    <ThumbUpIcon sx={{color:(emotion==="LIKE")?"red":"gray"}}/> {likeCount.LIKE}
                </IconButton>
                {/* 싫어요 아이콘 */}
                <IconButton onClick={handleLike}>
                    <ThumbDownIcon sx={{color:(emotion==="LIKE")?"red":"gray"}}/> {likeCount.LIKE}
                </IconButton>

                <ExpandMore
                    expand={expanded}
                    onClick={handleExpandClick}
                    aria-expanded={expanded}>
                    <ExpandMoreIcon />
                </ExpandMore>
            </CardActions>
            
            <Collapse in={expanded} timeout="auto" unmountOnExit>
                <Box sx={{ flexGrow: 1, padding:'1vh' }}>
                    {/* 댓글 입력창 */}
                    <Grid container spacing={2}>
                        <Grid item xs={11}>     
                            <Tooltip title="500자 내외로 댓글을 작성해주세요">
                                <TextField label={userComment?`${userComment.length}/500`:"댓글"} sx={{width:'100%'}}
                                variant="standard" multiline onChange={handleUserComment} value={userComment}/>                          
                            </Tooltip>                 
                        </Grid>
                            {/* 댓글 전송 */}
                        <Grid item xs={1}>
                            <Tooltip title="댓글작성">
                                <IconButton onClick={handleSumbitComment} disabled={isLoading} sx={{color:isLoading?"gray":"blue"}}>
                                    <SendIcon/>
                                </IconButton>
                            </Tooltip>
                        </Grid>
                    </Grid>
                </Box>
                <CardContent>                              
                    {/* 댓글 */}
                        {
                            comments.map((c, i)=>{
                                return (
                                    <Grid container key={i}>
                                        <Typography variant="strong" component="div">
                                            <strong>{c.content}</strong>
                                        </Typography>
                                        <Grid container>
                                            <Grid item xs={4}>
                                                <Typography sx={{ mb: 1.5 }} color="text.secondary">
                                                    by {c.nickname}
                                                </Typography>
                                            </Grid>
                                            <Grid item xs={4}>
                                            </Grid>
                                            <Grid item xs={4}>
                                                <Typography sx={{ mb: 1.5 }} color="text.secondary">
                                                    {c.createdAt}
                                                </Typography>
                                            </Grid>
                                        </Grid>                                          
                                    </Grid>
                                )
                            })
                        } 

                {/* 댓글페이지 */}
                <Box sx={{justifyContent:"center", display:"flex", marginTop:"5vh"}}>
                    <Pagination count={totalPage} defaultPage={currentPage+1} boundaryCount={5} onChange={handleCommentPage}
                        color="primary" size="large" sx={{margin: '2vh'}}/>
                </Box>
                </CardContent>

                
            </Collapse>
        </Card>
  );
}


export default DetailPost;

 

'Java > Spring' 카테고리의 다른 글

[Spring] 간단한 SNS 만들기 #8  (0) 2023.01.21
[Spring] 간단한 SNS 만들기 #7  (0) 2023.01.21
[Spring] 간단한 SNS 만들기 #5  (0) 2023.01.15
[Spring] 간단한 SNS 만들기 #4  (3) 2023.01.01
[Spring] 간단한 SNS 만들기 #3  (0) 2022.12.22