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 |